From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: Aikar Date: Mon, 29 Feb 2016 18:48:17 -0600 Subject: [PATCH] Timings v2 TODO: Add #isStopping to FullServerTickHandler#stopTiming in patch 191 expose isRunning diff --git a/src/main/java/co/aikar/timings/FullServerTickHandler.java b/src/main/java/co/aikar/timings/FullServerTickHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..43b85ce3a6c27a2f92c67d62bee7484c2652b72a --- /dev/null +++ b/src/main/java/co/aikar/timings/FullServerTickHandler.java @@ -0,0 +1,85 @@ +package co.aikar.timings; + +import static co.aikar.timings.TimingsManager.*; + +import org.bukkit.Bukkit; +import org.jetbrains.annotations.NotNull; + +public class FullServerTickHandler extends TimingHandler { + private static final TimingIdentifier IDENTITY = new TimingIdentifier("Minecraft", "Full Server Tick", null); + final TimingData minuteData; + double avgFreeMemory = -1D; + double avgUsedMemory = -1D; + FullServerTickHandler() { + super(IDENTITY); + minuteData = new TimingData(id); + + TIMING_MAP.put(IDENTITY, this); + } + + @NotNull + @Override + public Timing startTiming() { + if (TimingsManager.needsFullReset) { + TimingsManager.resetTimings(); + } else if (TimingsManager.needsRecheckEnabled) { + TimingsManager.recheckEnabled(); + } + return super.startTiming(); + } + + @Override + public void stopTiming() { + super.stopTiming(); + if (!isEnabled()) { + return; + } + if (TimingHistory.timedTicks % 20 == 0) { + final Runtime runtime = Runtime.getRuntime(); + double usedMemory = runtime.totalMemory() - runtime.freeMemory(); + double freeMemory = runtime.maxMemory() - usedMemory; + if (this.avgFreeMemory == -1) { + this.avgFreeMemory = freeMemory; + } else { + this.avgFreeMemory = (this.avgFreeMemory * (59 / 60D)) + (freeMemory * (1 / 60D)); + } + + if (this.avgUsedMemory == -1) { + this.avgUsedMemory = usedMemory; + } else { + this.avgUsedMemory = (this.avgUsedMemory * (59 / 60D)) + (usedMemory * (1 / 60D)); + } + } + + long start = System.nanoTime(); + TimingsManager.tick(); + long diff = System.nanoTime() - start; + TIMINGS_TICK.addDiff(diff, null); + // addDiff for TIMINGS_TICK incremented this, bring it back down to 1 per tick. + record.setCurTickCount(record.getCurTickCount()-1); + + minuteData.setCurTickTotal(record.getCurTickTotal()); + minuteData.setCurTickCount(1); + + boolean violated = isViolated(); + minuteData.processTick(violated); + TIMINGS_TICK.processTick(violated); + processTick(violated); + + + if (TimingHistory.timedTicks % 1200 == 0) { + MINUTE_REPORTS.add(new TimingHistory.MinuteReport()); + TimingHistory.resetTicks(false); + minuteData.reset(); + } + if (TimingHistory.timedTicks % Timings.getHistoryInterval() == 0) { + TimingsManager.HISTORY.add(new TimingHistory()); + TimingsManager.resetTimings(); + } + Bukkit.getUnsafe().reportTimings(); + } + + boolean isViolated() { + return record.getCurTickTotal() > 50000000; + } +} diff --git a/src/main/java/co/aikar/timings/NullTimingHandler.java b/src/main/java/co/aikar/timings/NullTimingHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..9b45ce887b9172f30302b83fe24b99b76b16dac3 --- /dev/null +++ b/src/main/java/co/aikar/timings/NullTimingHandler.java @@ -0,0 +1,68 @@ +/* + * This file is licensed under the MIT License (MIT). + * + * Copyright (c) 2014 Daniel Ennis + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package co.aikar.timings; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public final class NullTimingHandler implements Timing { + public static final Timing NULL = new NullTimingHandler(); + @NotNull + @Override + public Timing startTiming() { + return this; + } + + @Override + public void stopTiming() { + + } + + @NotNull + @Override + public Timing startTimingIfSync() { + return this; + } + + @Override + public void stopTimingIfSync() { + + } + + @Override + public void abort() { + + } + + @Nullable + @Override + public TimingHandler getTimingHandler() { + return null; + } + + @Override + public void close() { + + } +} diff --git a/src/main/java/co/aikar/timings/TimedEventExecutor.java b/src/main/java/co/aikar/timings/TimedEventExecutor.java new file mode 100644 index 0000000000000000000000000000000000000000..4e6e1b8e8aeb07e34536941d2cbfc25e5cfa6c27 --- /dev/null +++ b/src/main/java/co/aikar/timings/TimedEventExecutor.java @@ -0,0 +1,83 @@ +/* + * This file is licensed under the MIT License (MIT). + * + * Copyright (c) 2014 Daniel Ennis + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package co.aikar.timings; + +import org.bukkit.Bukkit; +import org.bukkit.event.Event; +import org.bukkit.event.EventException; +import org.bukkit.event.Listener; +import org.bukkit.plugin.EventExecutor; +import org.bukkit.plugin.Plugin; + +import java.lang.reflect.Method; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class TimedEventExecutor implements EventExecutor { + + private final EventExecutor executor; + private final Timing timings; + + /** + * Wraps an event executor and associates a timing handler to it. + * + * @param executor Executor to wrap + * @param plugin Owning plugin + * @param method EventHandler method + * @param eventClass Owning class + */ + public TimedEventExecutor(@NotNull EventExecutor executor, @NotNull Plugin plugin, @Nullable Method method, @NotNull Class eventClass) { + this.executor = executor; + String id; + + if (method == null) { + if (executor.getClass().getEnclosingClass() != null) { // Oh Skript, how we love you + method = executor.getClass().getEnclosingMethod(); + } + } + + if (method != null) { + id = method.getDeclaringClass().getName(); + } else { + id = executor.getClass().getName(); + } + + + final String eventName = eventClass.getSimpleName(); + boolean verbose = "BlockPhysicsEvent".equals(eventName); + this.timings = Timings.ofSafe(plugin, (verbose ? "## " : "") + + "Event: " + id + " (" + eventName + ")"); + } + + @Override + public void execute(@NotNull Listener listener, @NotNull Event event) throws EventException { + if (event.isAsynchronous() || !Timings.timingsEnabled || !Bukkit.isPrimaryThread()) { + executor.execute(listener, event); + return; + } + try (Timing ignored = timings.startTiming()){ + executor.execute(listener, event); + } + } +} diff --git a/src/main/java/co/aikar/timings/Timing.java b/src/main/java/co/aikar/timings/Timing.java new file mode 100644 index 0000000000000000000000000000000000000000..a21e5ead5024fd0058c5e3302d8201dd249d32bc --- /dev/null +++ b/src/main/java/co/aikar/timings/Timing.java @@ -0,0 +1,83 @@ +/* + * This file is licensed under the MIT License (MIT). + * + * Copyright (c) 2014 Daniel Ennis + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package co.aikar.timings; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * Provides an ability to time sections of code within the Minecraft Server + */ +public interface Timing extends AutoCloseable { + /** + * Starts timing the execution until {@link #stopTiming()} is called. + * + * @return Timing + */ + @NotNull + Timing startTiming(); + + /** + *

Stops timing and records the data. Propagates the data up to group handlers.

+ * + * Will automatically be called when this Timing is used with try-with-resources + */ + void stopTiming(); + + /** + * Starts timing the execution until {@link #stopTiming()} is called. + * + * But only if we are on the primary thread. + * + * @return Timing + */ + @NotNull + Timing startTimingIfSync(); + + /** + *

Stops timing and records the data. Propagates the data up to group handlers.

+ * + *

Will automatically be called when this Timing is used with try-with-resources

+ * + * But only if we are on the primary thread. + */ + void stopTimingIfSync(); + + /** + * @deprecated Doesn't do anything - Removed + */ + @Deprecated + void abort(); + + /** + * Used internally to get the actual backing Handler in the case of delegated Handlers + * + * @return TimingHandler + */ + @Nullable + TimingHandler getTimingHandler(); + + @Override + void close(); +} diff --git a/src/main/java/co/aikar/timings/TimingData.java b/src/main/java/co/aikar/timings/TimingData.java new file mode 100644 index 0000000000000000000000000000000000000000..a5d13a1e44edb861f45c83a9b4309fbf799d407d --- /dev/null +++ b/src/main/java/co/aikar/timings/TimingData.java @@ -0,0 +1,122 @@ +/* + * This file is licensed under the MIT License (MIT). + * + * Copyright (c) 2014 Daniel Ennis + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package co.aikar.timings; + +import java.util.List; +import org.jetbrains.annotations.NotNull; + +import static co.aikar.util.JSONUtil.toArray; + +/** + *

Lightweight object for tracking timing data

+ * + * This is broken out to reduce memory usage + */ +class TimingData { + private final int id; + private int count = 0; + private int lagCount = 0; + private long totalTime = 0; + private long lagTotalTime = 0; + private int curTickCount = 0; + private long curTickTotal = 0; + + TimingData(int id) { + this.id = id; + } + + private TimingData(TimingData data) { + this.id = data.id; + this.totalTime = data.totalTime; + this.lagTotalTime = data.lagTotalTime; + this.count = data.count; + this.lagCount = data.lagCount; + } + + void add(long diff) { + ++curTickCount; + curTickTotal += diff; + } + + void processTick(boolean violated) { + totalTime += curTickTotal; + count += curTickCount; + if (violated) { + lagTotalTime += curTickTotal; + lagCount += curTickCount; + } + curTickTotal = 0; + curTickCount = 0; + } + + void reset() { + count = 0; + lagCount = 0; + curTickTotal = 0; + curTickCount = 0; + totalTime = 0; + lagTotalTime = 0; + } + + protected TimingData clone() { + return new TimingData(this); + } + + @NotNull + List export() { + List list = toArray( + id, + count, + totalTime); + if (lagCount > 0) { + list.add(lagCount); + list.add(lagTotalTime); + } + return list; + } + + boolean hasData() { + return count > 0; + } + + long getTotalTime() { + return totalTime; + } + + int getCurTickCount() { + return curTickCount; + } + + void setCurTickCount(int curTickCount) { + this.curTickCount = curTickCount; + } + + long getCurTickTotal() { + return curTickTotal; + } + + void setCurTickTotal(long curTickTotal) { + this.curTickTotal = curTickTotal; + } +} diff --git a/src/main/java/co/aikar/timings/TimingHandler.java b/src/main/java/co/aikar/timings/TimingHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..199789d56d22fcb1b77ebd56805cc28aa5a5ab0a --- /dev/null +++ b/src/main/java/co/aikar/timings/TimingHandler.java @@ -0,0 +1,226 @@ +/* + * This file is licensed under the MIT License (MIT). + * + * Copyright (c) 2014 Daniel Ennis + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package co.aikar.timings; + +import co.aikar.util.LoadingIntMap; +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; + +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.bukkit.Bukkit; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +class TimingHandler implements Timing { + + private static AtomicInteger idPool = new AtomicInteger(1); + private static Deque TIMING_STACK = new ArrayDeque<>(); + final int id = idPool.getAndIncrement(); + + final TimingIdentifier identifier; + private final boolean verbose; + + private final Int2ObjectOpenHashMap children = new LoadingIntMap<>(TimingData::new); + + final TimingData record; + private TimingHandler startParent; + private final TimingHandler groupHandler; + + private long start = 0; + private int timingDepth = 0; + private boolean added; + private boolean timed; + private boolean enabled; + + TimingHandler(@NotNull TimingIdentifier id) { + this.identifier = id; + this.verbose = id.name.startsWith("##"); + this.record = new TimingData(this.id); + this.groupHandler = id.groupHandler; + + TimingIdentifier.getGroup(id.group).handlers.add(this); + checkEnabled(); + } + + final void checkEnabled() { + enabled = Timings.timingsEnabled && (!verbose || Timings.verboseEnabled); + } + + void processTick(boolean violated) { + if (timingDepth != 0 || record.getCurTickCount() == 0) { + timingDepth = 0; + start = 0; + return; + } + + record.processTick(violated); + for (TimingData handler : children.values()) { + handler.processTick(violated); + } + } + + @NotNull + @Override + public Timing startTimingIfSync() { + startTiming(); + return this; + } + + @Override + public void stopTimingIfSync() { + stopTiming(); + } + + @NotNull + public Timing startTiming() { + if (!enabled || !Bukkit.isPrimaryThread()) { + return this; + } + if (++timingDepth == 1) { + startParent = TIMING_STACK.peekLast(); + start = System.nanoTime(); + } + TIMING_STACK.addLast(this); + return this; + } + + public void stopTiming() { + if (!enabled || timingDepth <= 0 || start == 0 || !Bukkit.isPrimaryThread()) { + return; + } + + popTimingStack(); + if (--timingDepth == 0) { + addDiff(System.nanoTime() - start, startParent); + startParent = null; + start = 0; + } + } + + private void popTimingStack() { + TimingHandler last; + while ((last = TIMING_STACK.removeLast()) != this) { + last.timingDepth = 0; + if ("Minecraft".equalsIgnoreCase(last.identifier.group)) { + Logger.getGlobal().log(Level.SEVERE, "TIMING_STACK_CORRUPTION - Look above this for any errors and report this to Paper unless it has a plugin in the stack trace (" + last.identifier + " did not stopTiming)"); + } else { + Logger.getGlobal().log(Level.SEVERE, "TIMING_STACK_CORRUPTION - Report this to the plugin " + last.identifier.group + " (Look for errors above this in the logs) (" + last.identifier + " did not stopTiming)", new Throwable()); + } + + boolean found = TIMING_STACK.contains(this); + if (!found) { + // We aren't even in the stack... Don't pop everything + TIMING_STACK.addLast(last); + break; + } + } + } + + @Override + public final void abort() { + + } + + void addDiff(long diff, @Nullable TimingHandler parent) { + if (parent != null) { + parent.children.get(id).add(diff); + } + + record.add(diff); + if (!added) { + added = true; + timed = true; + TimingsManager.HANDLERS.add(this); + } + if (groupHandler != null) { + groupHandler.addDiff(diff, parent); + groupHandler.children.get(id).add(diff); + } + } + + /** + * Reset this timer, setting all values to zero. + */ + void reset(boolean full) { + record.reset(); + if (full) { + timed = false; + } + start = 0; + timingDepth = 0; + added = false; + children.clear(); + checkEnabled(); + } + + @NotNull + @Override + public TimingHandler getTimingHandler() { + return this; + } + + @Override + public boolean equals(Object o) { + return (this == o); + } + + @Override + public int hashCode() { + return id; + } + + /** + * This is simply for the Closeable interface so it can be used with try-with-resources () + */ + @Override + public void close() { + stopTimingIfSync(); + } + + public boolean isSpecial() { + return this == TimingsManager.FULL_SERVER_TICK || this == TimingsManager.TIMINGS_TICK; + } + + boolean isTimed() { + return timed; + } + + public boolean isEnabled() { + return enabled; + } + + @NotNull + TimingData[] cloneChildren() { + final TimingData[] clonedChildren = new TimingData[children.size()]; + int i = 0; + for (TimingData child : children.values()) { + clonedChildren[i++] = child.clone(); + } + return clonedChildren; + } +} diff --git a/src/main/java/co/aikar/timings/TimingHistory.java b/src/main/java/co/aikar/timings/TimingHistory.java new file mode 100644 index 0000000000000000000000000000000000000000..ddaed81275fcc12d1671b668697acf318e96888b --- /dev/null +++ b/src/main/java/co/aikar/timings/TimingHistory.java @@ -0,0 +1,354 @@ +/* + * This file is licensed under the MIT License (MIT). + * + * Copyright (c) 2014 Daniel Ennis + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package co.aikar.timings; + +import co.aikar.timings.TimingHistory.RegionData.RegionId; +import com.google.common.base.Function; +import com.google.common.collect.Sets; +import org.bukkit.Bukkit; +import org.bukkit.Chunk; +import org.bukkit.Material; +import org.bukkit.World; +import org.bukkit.block.BlockState; +import org.bukkit.entity.Entity; +import org.bukkit.entity.EntityType; +import org.bukkit.entity.Player; +import co.aikar.util.LoadingMap; +import co.aikar.util.MRUMapCache; + +import java.lang.management.ManagementFactory; +import java.util.Collection; +import java.util.EnumMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import static co.aikar.timings.TimingsManager.FULL_SERVER_TICK; +import static co.aikar.timings.TimingsManager.MINUTE_REPORTS; +import static co.aikar.util.JSONUtil.*; + +@SuppressWarnings({"deprecation", "SuppressionAnnotation", "Convert2Lambda", "Anonymous2MethodRef"}) +public class TimingHistory { + public static long lastMinuteTime; + public static long timedTicks; + public static long playerTicks; + public static long entityTicks; + public static long tileEntityTicks; + public static long activatedEntityTicks; + private static int worldIdPool = 1; + static Map worldMap = LoadingMap.newHashMap(new Function() { + @NotNull + @Override + public Integer apply(@Nullable String input) { + return worldIdPool++; + } + }); + private final long endTime; + private final long startTime; + private final long totalTicks; + private final long totalTime; // Represents all time spent running the server this history + private final MinuteReport[] minuteReports; + + private final TimingHistoryEntry[] entries; + final Set tileEntityTypeSet = Sets.newHashSet(); + final Set entityTypeSet = Sets.newHashSet(); + private final Map 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() { + @NotNull + @Override + public JSONPair apply(World world) { + Map 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() { + @NotNull + @Override + public Object apply(RegionData input) { + return toArray( + input.regionId.x, + input.regionId.z, + toObjectMapper(input.entityCounts.entrySet(), + new Function, JSONPair>() { + @NotNull + @Override + public JSONPair apply(Map.Entry entry) { + entityTypeSet.add(entry.getKey()); + return pair( + String.valueOf(entry.getKey().ordinal()), + entry.getValue().count() + ); + } + } + ), + toObjectMapper(input.tileEntityCounts.entrySet(), + new Function, JSONPair>() { + @NotNull + @Override + public JSONPair apply(Map.Entry entry) { + tileEntityTypeSet.add(entry.getKey()); + return pair( + String.valueOf(entry.getKey().ordinal()), + entry.getValue().count() + ); + } + } + ) + ); + } + }) + ); + } + }); + } + static class RegionData { + final RegionId regionId; + @SuppressWarnings("Guava") + static Function LOADER = new Function() { + @NotNull + @Override + public RegionData apply(@NotNull RegionId id) { + return new RegionData(id); + } + }; + RegionData(@NotNull RegionId id) { + this.regionId = id; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + RegionData that = (RegionData) o; + + return regionId.equals(that.regionId); + + } + + @Override + public int hashCode() { + return regionId.hashCode(); + } + + @SuppressWarnings("unchecked") + final Map entityCounts = MRUMapCache.of(LoadingMap.of( + new EnumMap(EntityType.class), k -> new Counter() + )); + @SuppressWarnings("unchecked") + final Map tileEntityCounts = MRUMapCache.of(LoadingMap.of( + new EnumMap(Material.class), k -> new Counter() + )); + + static class RegionId { + final int x, z; + final long regionId; + RegionId(int x, int z) { + this.x = x >> 5 << 5; + this.z = z >> 5 << 5; + this.regionId = ((long) (this.x) << 32) + (this.z >> 5 << 5) - Integer.MIN_VALUE; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + RegionId regionId1 = (RegionId) o; + + return regionId == regionId1.regionId; + + } + + @Override + public int hashCode() { + return (int) (regionId ^ (regionId >>> 32)); + } + } + } + static void resetTicks(boolean fullReset) { + if (fullReset) { + // Non full is simply for 1 minute reports + timedTicks = 0; + } + lastMinuteTime = System.nanoTime(); + playerTicks = 0; + tileEntityTicks = 0; + entityTicks = 0; + activatedEntityTicks = 0; + } + + @NotNull + Object export() { + return createObject( + pair("s", startTime), + pair("e", endTime), + pair("tk", totalTicks), + pair("tm", totalTime), + pair("w", worlds), + pair("h", toArrayMapper(entries, new Function() { + @Nullable + @Override + public Object apply(TimingHistoryEntry entry) { + TimingData record = entry.data; + if (!record.hasData()) { + return null; + } + return entry.export(); + } + })), + pair("mp", toArrayMapper(minuteReports, new Function() { + @NotNull + @Override + public Object apply(MinuteReport input) { + return input.export(); + } + })) + ); + } + + static class MinuteReport { + final long time = System.currentTimeMillis() / 1000; + + final TicksRecord ticksRecord = new TicksRecord(); + final PingRecord pingRecord = new PingRecord(); + final TimingData fst = TimingsManager.FULL_SERVER_TICK.minuteData.clone(); + final double tps = 1E9 / ( System.nanoTime() - lastMinuteTime ) * ticksRecord.timed; + final double usedMemory = TimingsManager.FULL_SERVER_TICK.avgUsedMemory; + final double freeMemory = TimingsManager.FULL_SERVER_TICK.avgFreeMemory; + final double loadAvg = ManagementFactory.getOperatingSystemMXBean().getSystemLoadAverage(); + + @NotNull + List 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 onlinePlayers = Bukkit.getOnlinePlayers(); + int totalPing = 0; + for (Player player : onlinePlayers) { + totalPing += player.spigot().getPing(); + } + avg = onlinePlayers.isEmpty() ? 0 : totalPing / onlinePlayers.size(); + } + } + + + private static class Counter { + private int count = 0; + public int increment() { + return ++count; + } + public int count() { + return count; + } + } +} diff --git a/src/main/java/co/aikar/timings/TimingHistoryEntry.java b/src/main/java/co/aikar/timings/TimingHistoryEntry.java new file mode 100644 index 0000000000000000000000000000000000000000..86d5ac6bd0d7d0003688761aceb3f3343575319f --- /dev/null +++ b/src/main/java/co/aikar/timings/TimingHistoryEntry.java @@ -0,0 +1,58 @@ +/* + * This file is licensed under the MIT License (MIT). + * + * Copyright (c) 2014 Daniel Ennis + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package co.aikar.timings; + +import com.google.common.base.Function; + +import java.util.List; +import org.jetbrains.annotations.NotNull; + +import static co.aikar.util.JSONUtil.toArrayMapper; + +class TimingHistoryEntry { + final TimingData data; + private final TimingData[] children; + + TimingHistoryEntry(@NotNull TimingHandler handler) { + this.data = handler.record.clone(); + children = handler.cloneChildren(); + } + + @NotNull + List export() { + List result = data.export(); + if (children.length > 0) { + result.add( + toArrayMapper(children, new Function() { + @NotNull + @Override + public Object apply(TimingData child) { + return child.export(); + } + }) + ); + } + return result; + } +} diff --git a/src/main/java/co/aikar/timings/TimingIdentifier.java b/src/main/java/co/aikar/timings/TimingIdentifier.java new file mode 100644 index 0000000000000000000000000000000000000000..df142a89b8c43acb81eb383eac0ef048a1f49a6e --- /dev/null +++ b/src/main/java/co/aikar/timings/TimingIdentifier.java @@ -0,0 +1,116 @@ +/* + * This file is licensed under the MIT License (MIT). + * + * Copyright (c) 2014 Daniel Ennis + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package co.aikar.timings; + +import co.aikar.util.LoadingMap; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + *

Used as a basis for fast HashMap key comparisons for the Timing Map.

+ * + * 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 GROUP_MAP = LoadingMap.of(new ConcurrentHashMap<>(64, .5F), TimingGroup::new); + private static final TimingGroup DEFAULT_GROUP = getGroup("Minecraft"); + final String group; + final String name; + final TimingHandler groupHandler; + private final int hashCode; + + TimingIdentifier(@Nullable String group, @NotNull String name, @Nullable Timing groupHandler) { + this.group = group != null ? group: DEFAULT_GROUP.name; + this.name = name; + this.groupHandler = groupHandler != null ? groupHandler.getTimingHandler() : null; + this.hashCode = (31 * this.group.hashCode()) + this.name.hashCode(); + } + + @NotNull + static TimingGroup getGroup(@Nullable String groupName) { + if (groupName == null) { + //noinspection ConstantConditions + return DEFAULT_GROUP; + } + + return GROUP_MAP.get(groupName); + } + + @Override + public boolean equals(Object o) { + if (o == null) { + return false; + } + + TimingIdentifier that = (TimingIdentifier) o; + return Objects.equals(group, that.group) && Objects.equals(name, that.name); + } + + @Override + public int hashCode() { + return hashCode; + } + + @Override + public String toString() { + return "TimingIdentifier{id=" + group + ":" + name +'}'; + } + + static class TimingGroup { + + private static AtomicInteger idPool = new AtomicInteger(1); + final int id = idPool.getAndIncrement(); + + final String name; + final List handlers = Collections.synchronizedList(new ArrayList<>(64)); + + private TimingGroup(String name) { + this.name = name; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + TimingGroup that = (TimingGroup) o; + return id == that.id; + } + + @Override + public int hashCode() { + return id; + } + } +} diff --git a/src/main/java/co/aikar/timings/Timings.java b/src/main/java/co/aikar/timings/Timings.java new file mode 100644 index 0000000000000000000000000000000000000000..dd72a34eaa4bedd9ea0b92eaa79091b00eb4dd09 --- /dev/null +++ b/src/main/java/co/aikar/timings/Timings.java @@ -0,0 +1,295 @@ +/* + * This file is licensed under the MIT License (MIT). + * + * Copyright (c) 2014 Daniel Ennis + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package co.aikar.timings; + +import com.google.common.base.Preconditions; +import com.google.common.collect.EvictingQueue; +import com.google.common.collect.Lists; +import org.bukkit.Bukkit; +import org.bukkit.command.CommandSender; +import org.bukkit.plugin.Plugin; + +import java.util.List; +import java.util.Queue; +import java.util.logging.Level; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +@SuppressWarnings({"UnusedDeclaration", "WeakerAccess", "SameParameterValue"}) +public final class Timings { + + final static List requestingReport = Lists.newArrayList(); + private static final int MAX_HISTORY_FRAMES = 12; + public static final Timing NULL_HANDLER = new NullTimingHandler(); + static boolean timingsEnabled = false; + static boolean verboseEnabled = false; + private static int historyInterval = -1; + private static int historyLength = -1; + + private Timings() {} + + /** + * Returns a Timing for a plugin corresponding to a name. + * + * @param plugin Plugin to own the Timing + * @param name Name of Timing + * @return Handler + */ + @NotNull + public static Timing of(@NotNull Plugin plugin, @NotNull String name) { + Timing pluginHandler = null; + if (plugin != null) { + pluginHandler = ofSafe(plugin.getName(), "Combined Total", TimingsManager.PLUGIN_GROUP_HANDLER); + } + return of(plugin, name, pluginHandler); + } + + /** + *

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.

+ * + * Parent Timers are used to group multiple subsections together and get a summary of them combined + * Parent Handler can not be changed after first call + * + * @param plugin Plugin to own the Timing + * @param name Name of Timing + * @param groupHandler Parent handler to mirror .start/stop calls to + * @return Timing Handler + */ + @NotNull + public static Timing of(@NotNull Plugin plugin, @NotNull String name, @Nullable Timing groupHandler) { + Preconditions.checkNotNull(plugin, "Plugin can not be null"); + return TimingsManager.getHandler(plugin.getName(), name, groupHandler); + } + + /** + * Returns a Timing object after starting it, useful for Java7 try-with-resources. + * + * try (Timing ignored = Timings.ofStart(plugin, someName)) { + * // timed section + * } + * + * @param plugin Plugin to own the Timing + * @param name Name of Timing + * @return Timing Handler + */ + @NotNull + public static Timing ofStart(@NotNull Plugin plugin, @NotNull String name) { + return ofStart(plugin, name, null); + } + + /** + * Returns a Timing object after starting it, useful for Java7 try-with-resources. + * + * try (Timing ignored = Timings.ofStart(plugin, someName, groupHandler)) { + * // timed section + * } + * + * @param plugin Plugin to own the Timing + * @param name Name of Timing + * @param groupHandler Parent handler to mirror .start/stop calls to + * @return Timing Handler + */ + @NotNull + public static Timing ofStart(@NotNull Plugin plugin, @NotNull String name, @Nullable Timing groupHandler) { + Timing timing = of(plugin, name, groupHandler); + timing.startTiming(); + return timing; + } + + /** + * Gets whether or not the Spigot Timings system is enabled + * + * @return Enabled or not + */ + public static boolean isTimingsEnabled() { + return timingsEnabled; + } + + /** + *

Sets whether or not the Spigot Timings system should be enabled

+ * + * Calling this will reset timing data. + * + * @param enabled Should timings be reported + */ + public static void setTimingsEnabled(boolean enabled) { + timingsEnabled = enabled; + reset(); + } + + /** + *

Sets whether or not the Timings should monitor at Verbose level.

+ * + *

When Verbose is disabled, high-frequency timings will not be available.

+ * + * @return Enabled or not + */ + public static boolean isVerboseTimingsEnabled() { + return verboseEnabled; + } + + /** + *

Sets whether or not the Timings should monitor at Verbose level.

+ * + * 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; + } + + /** + *

Gets the interval between Timing History report generation.

+ * + * Defaults to 5 minutes (6000 ticks) + * + * @return Interval in ticks + */ + public static int getHistoryInterval() { + return historyInterval; + } + + /** + *

Sets the interval between Timing History report generations.

+ * + *

Defaults to 5 minutes (6000 ticks)

+ * + * 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 oldQueue = TimingsManager.HISTORY; + int frames = (getHistoryLength() / getHistoryInterval()); + if (length > maxLength) { + Bukkit.getLogger().log(Level.WARNING, "Timings Length too high. Requested " + length + ", max is " + maxLength + ". To get longer history, you must increase your interval. Set Interval to " + Math.ceil(length / MAX_HISTORY_FRAMES) + " to achieve this length."); + } + TimingsManager.HISTORY = EvictingQueue.create(frames); + TimingsManager.HISTORY.addAll(oldQueue); + } + + /** + * Resets all Timing Data + */ + public static void reset() { + TimingsManager.reset(); + } + + /** + * Generates a report and sends it to the specified command sender. + * + * If sender is null, ConsoleCommandSender will be used. + * @param sender The sender to send to, or null to use the ConsoleCommandSender + */ + public static void generateReport(@Nullable CommandSender sender) { + if (sender == null) { + sender = Bukkit.getConsoleSender(); + } + requestingReport.add(sender); + } + + /** + * Generates a report and sends it to the specified listener. + * Use with {@link org.bukkit.command.BufferedCommandSender} to get full response when done! + * @param sender The listener to send responses too. + */ + public static void generateReport(@NotNull TimingsReportListener sender) { + Preconditions.checkNotNull(sender); + requestingReport.add(sender); + } + + /* + ================= + Protected API: These are for internal use only in Bukkit/CraftBukkit + These do not have isPrimaryThread() checks in the startTiming/stopTiming + ================= + */ + @NotNull + static TimingHandler ofSafe(@NotNull String name) { + return ofSafe(null, name, null); + } + + @NotNull + static Timing ofSafe(@Nullable Plugin plugin, @NotNull String name) { + Timing pluginHandler = null; + if (plugin != null) { + pluginHandler = ofSafe(plugin.getName(), "Combined Total", TimingsManager.PLUGIN_GROUP_HANDLER); + } + return ofSafe(plugin != null ? plugin.getName() : "Minecraft - Invalid Plugin", name, pluginHandler); + } + + @NotNull + static TimingHandler ofSafe(@NotNull String name, @Nullable Timing groupHandler) { + return ofSafe(null, name, groupHandler); + } + + @NotNull + static TimingHandler ofSafe(@Nullable String groupName, @NotNull String name, @Nullable Timing groupHandler) { + return TimingsManager.getHandler(groupName, name, groupHandler); + } +} diff --git a/src/main/java/co/aikar/timings/TimingsCommand.java b/src/main/java/co/aikar/timings/TimingsCommand.java new file mode 100644 index 0000000000000000000000000000000000000000..3132dc98d26c54c5e46162e53aaed195d7335c8d --- /dev/null +++ b/src/main/java/co/aikar/timings/TimingsCommand.java @@ -0,0 +1,120 @@ +/* + * This file is licensed under the MIT License (MIT). + * + * Copyright (c) 2014 Daniel Ennis + * + * 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.ImmutableList; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.command.CommandSender; +import org.bukkit.command.defaults.BukkitCommand; +import org.bukkit.util.StringUtil; + +import java.util.ArrayList; +import java.util.List; +import org.jetbrains.annotations.NotNull; + +import static net.kyori.adventure.text.Component.text; + + +public class TimingsCommand extends BukkitCommand { + private static final List TIMINGS_SUBCOMMANDS = ImmutableList.of("report", "reset", "on", "off", "paste", "verbon", "verboff"); + private long lastResetAttempt = 0; + + public TimingsCommand(@NotNull String name) { + super(name); + this.description = "Manages Spigot Timings data to see performance of the server."; + this.usageMessage = "/timings "; + this.setPermission("bukkit.command.timings"); + } + + @Override + public boolean execute(@NotNull CommandSender sender, @NotNull String currentAlias, @NotNull String[] args) { + if (!testPermission(sender)) { + return true; + } + if (args.length < 1) { + sender.sendMessage(text("Usage: " + this.usageMessage, NamedTextColor.RED)); + return true; + } + final String arg = args[0]; + if ("on".equalsIgnoreCase(arg)) { + Timings.setTimingsEnabled(true); + sender.sendMessage(text("Enabled Timings & Reset")); + return true; + } else if ("off".equalsIgnoreCase(arg)) { + Timings.setTimingsEnabled(false); + sender.sendMessage(text("Disabled Timings")); + return true; + } + + if (!Timings.isTimingsEnabled()) { + sender.sendMessage(text("Please enable timings by typing /timings on")); + return true; + } + + long now = System.currentTimeMillis(); + if ("verbon".equalsIgnoreCase(arg)) { + Timings.setVerboseTimingsEnabled(true); + sender.sendMessage(text("Enabled Verbose Timings")); + return true; + } else if ("verboff".equalsIgnoreCase(arg)) { + Timings.setVerboseTimingsEnabled(false); + sender.sendMessage(text("Disabled Verbose Timings")); + return true; + } else if ("reset".equalsIgnoreCase(arg)) { + if (now - lastResetAttempt < 30000) { + TimingsManager.reset(); + sender.sendMessage(text("Timings reset. Please wait 5-10 minutes before using /timings report.", NamedTextColor.RED)); + } else { + lastResetAttempt = now; + sender.sendMessage(text("WARNING: Timings v2 should not be reset. If you are experiencing 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.", NamedTextColor.RED)); + } + } else if ( + "paste".equalsIgnoreCase(arg) || + "report".equalsIgnoreCase(arg) || + "get".equalsIgnoreCase(arg) || + "merged".equalsIgnoreCase(arg) || + "separate".equalsIgnoreCase(arg) + ) { + Timings.generateReport(sender); + } else { + sender.sendMessage(text("Usage: " + this.usageMessage, NamedTextColor.RED)); + } + return true; + } + + @NotNull + @Override + public List tabComplete(@NotNull CommandSender sender, @NotNull String alias, @NotNull String[] args) { + Preconditions.checkNotNull(sender, "Sender cannot be null"); + Preconditions.checkNotNull(args, "Arguments cannot be null"); + Preconditions.checkNotNull(alias, "Alias cannot be null"); + + if (args.length == 1) { + return StringUtil.copyPartialMatches(args[0], TIMINGS_SUBCOMMANDS, + new ArrayList(TIMINGS_SUBCOMMANDS.size())); + } + return ImmutableList.of(); + } +} diff --git a/src/main/java/co/aikar/timings/TimingsManager.java b/src/main/java/co/aikar/timings/TimingsManager.java new file mode 100644 index 0000000000000000000000000000000000000000..a92925d41110226f7fda055b71ce7be60eedd038 --- /dev/null +++ b/src/main/java/co/aikar/timings/TimingsManager.java @@ -0,0 +1,188 @@ +/* + * This file is licensed under the MIT License (MIT). + * + * Copyright (c) 2014 Daniel Ennis + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package co.aikar.timings; + +import co.aikar.util.LoadingMap; +import com.google.common.collect.EvictingQueue; +import org.bukkit.Bukkit; +import org.bukkit.Server; +import org.bukkit.command.Command; +import org.bukkit.plugin.Plugin; +import org.bukkit.plugin.java.PluginClassLoader; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.logging.Level; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public final class TimingsManager { + static final Map 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 String url = "https://timings.aikar.co/"; + public static List hiddenConfigs = new ArrayList(); + public static boolean privacy = false; + + static final List HANDLERS = new ArrayList<>(1024); + static final List MINUTE_REPORTS = new ArrayList<>(64); + + static EvictingQueue HISTORY = EvictingQueue.create(12); + static long timingStart = 0; + static long historyStart = 0; + static boolean needsFullReset = false; + static boolean needsRecheckEnabled = false; + + private TimingsManager() {} + + /** + * Resets all timing data on the next tick + */ + static void reset() { + needsFullReset = true; + } + + /** + * Ticked every tick by CraftBukkit to count the number of times a timer + * caused TPS loss. + */ + static void tick() { + if (Timings.timingsEnabled) { + boolean violated = FULL_SERVER_TICK.isViolated(); + + for (TimingHandler handler : HANDLERS) { + if (handler.isSpecial()) { + // We manually call this + continue; + } + handler.processTick(violated); + } + + TimingHistory.playerTicks += Bukkit.getOnlinePlayers().size(); + TimingHistory.timedTicks++; + // Generate TPS/Ping/Tick reports every minute + } + } + static void stopServer() { + Timings.timingsEnabled = false; + recheckEnabled(); + } + static void recheckEnabled() { + synchronized (TIMING_MAP) { + for (TimingHandler timings : TIMING_MAP.values()) { + timings.checkEnabled(); + } + } + needsRecheckEnabled = false; + } + static void resetTimings() { + if (needsFullReset) { + // Full resets need to re-check every handlers enabled state + // Timing map can be modified from async so we must sync on it. + synchronized (TIMING_MAP) { + for (TimingHandler timings : TIMING_MAP.values()) { + timings.reset(true); + } + } + Bukkit.getLogger().log(Level.INFO, "Timings Reset"); + HISTORY.clear(); + needsFullReset = false; + needsRecheckEnabled = false; + timingStart = System.currentTimeMillis(); + } else { + // Soft resets only need to act on timings that have done something + // Handlers can only be modified on main thread. + for (TimingHandler timings : HANDLERS) { + timings.reset(false); + } + } + + HANDLERS.clear(); + MINUTE_REPORTS.clear(); + + TimingHistory.resetTicks(true); + historyStart = System.currentTimeMillis(); + } + + @NotNull + static TimingHandler getHandler(@Nullable String group, @NotNull String name, @Nullable Timing parent) { + return TIMING_MAP.get(new TimingIdentifier(group, name, parent)); + } + + + /** + *

Due to access restrictions, we need a helper method to get a Command TimingHandler with String group

+ * + * Plugins should never call this + * + * @param pluginName Plugin this command is associated with + * @param command Command to get timings for + * @return TimingHandler + */ + @NotNull + public static Timing getCommandTiming(@Nullable String pluginName, @NotNull Command command) { + Plugin plugin = null; + final Server server = Bukkit.getServer(); + if (!( server == null || pluginName == null || + "minecraft".equals(pluginName) || "bukkit".equals(pluginName) || + "spigot".equalsIgnoreCase(pluginName) || "paper".equals(pluginName) + )) { + plugin = server.getPluginManager().getPlugin(pluginName); + } + if (plugin == null) { + // Plugin is passing custom fallback prefix, try to look up by class loader + plugin = getPluginByClassloader(command.getClass()); + } + if (plugin == null) { + return Timings.ofSafe("Command: " + pluginName + ":" + command.getTimingName()); + } + + return Timings.ofSafe(plugin, "Command: " + pluginName + ":" + command.getTimingName()); + } + + /** + * Looks up the class loader for the specified class, and if it is a PluginClassLoader, return the + * Plugin that created this class. + * + * @param clazz Class to check + * @return Plugin if created by a plugin + */ + @Nullable + public static Plugin getPluginByClassloader(@Nullable Class clazz) { + if (clazz == null) { + return null; + } + final ClassLoader classLoader = clazz.getClassLoader(); + if (classLoader instanceof PluginClassLoader) { + PluginClassLoader pluginClassLoader = (PluginClassLoader) classLoader; + return pluginClassLoader.getPlugin(); + } + return null; + } +} diff --git a/src/main/java/co/aikar/timings/TimingsReportListener.java b/src/main/java/co/aikar/timings/TimingsReportListener.java new file mode 100644 index 0000000000000000000000000000000000000000..34f4c02c3bdbe571a7efb1f8c61d8924b0c81268 --- /dev/null +++ b/src/main/java/co/aikar/timings/TimingsReportListener.java @@ -0,0 +1,86 @@ +package co.aikar.timings; + +import com.google.common.base.Preconditions; +import com.google.common.collect.Lists; +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 org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +@SuppressWarnings("WeakerAccess") +public class TimingsReportListener implements net.kyori.adventure.audience.ForwardingAudience, MessageCommandSender { + private final List senders; + private final Runnable onDone; + private String timingsURL; + + public TimingsReportListener(@NotNull CommandSender senders) { + this(senders, null); + } + public TimingsReportListener(@NotNull CommandSender sender, @Nullable Runnable onDone) { + this(Lists.newArrayList(sender), onDone); + } + public TimingsReportListener(@NotNull List senders) { + this(senders, null); + } + public TimingsReportListener(@NotNull List senders, @Nullable Runnable onDone) { + Preconditions.checkNotNull(senders); + Preconditions.checkArgument(!senders.isEmpty(), "senders is empty"); + + this.senders = Lists.newArrayList(senders); + this.onDone = onDone; + } + + @Nullable + public String getTimingsURL() { + return timingsURL; + } + + public void done() { + done(null); + } + + public void done(@Nullable String url) { + this.timingsURL = url; + if (onDone != null) { + onDone.run(); + } + for (CommandSender sender : senders) { + if (sender instanceof TimingsReportListener) { + ((TimingsReportListener) sender).done(); + } + } + } + + @Override + public void sendMessage(final @NotNull net.kyori.adventure.identity.Identity source, final @NotNull net.kyori.adventure.text.Component message, final @NotNull net.kyori.adventure.audience.MessageType type) { + net.kyori.adventure.audience.ForwardingAudience.super.sendMessage(source, message, type); + } + + @NotNull + @Override + public Iterable audiences() { + return this.senders; + } + + @Override + public void sendMessage(@NotNull String message) { + senders.forEach((sender) -> sender.sendMessage(message)); + } + + public void addConsoleIfNeeded() { + boolean hasConsole = false; + for (CommandSender sender : this.senders) { + if (sender instanceof ConsoleCommandSender || sender instanceof RemoteConsoleCommandSender) { + hasConsole = true; + } + } + if (!hasConsole) { + this.senders.add(Bukkit.getConsoleSender()); + } + } +} diff --git a/src/main/java/co/aikar/timings/UnsafeTimingHandler.java b/src/main/java/co/aikar/timings/UnsafeTimingHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..632c4961515f5052551f841cfa840e60bba7a257 --- /dev/null +++ b/src/main/java/co/aikar/timings/UnsafeTimingHandler.java @@ -0,0 +1,53 @@ +/* + * This file is licensed under the MIT License (MIT). + * + * Copyright (c) 2014 Daniel Ennis + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package co.aikar.timings; + +import org.bukkit.Bukkit; +import org.jetbrains.annotations.NotNull; + +class UnsafeTimingHandler extends TimingHandler { + + UnsafeTimingHandler(@NotNull TimingIdentifier id) { + super(id); + } + + private static void checkThread() { + if (!Bukkit.isPrimaryThread()) { + throw new IllegalStateException("Calling Timings from Async Operation"); + } + } + + @NotNull + @Override + public Timing startTiming() { + checkThread(); + return super.startTiming(); + } + + @Override + public void stopTiming() { + checkThread(); + super.stopTiming(); + } +} diff --git a/src/main/java/co/aikar/util/Counter.java b/src/main/java/co/aikar/util/Counter.java new file mode 100644 index 0000000000000000000000000000000000000000..eff63c371c39e21a5a9cb5af8c2dcf78a60dd52b --- /dev/null +++ b/src/main/java/co/aikar/util/Counter.java @@ -0,0 +1,38 @@ +package co.aikar.util; + +import com.google.common.collect.ForwardingMap; + +import java.util.HashMap; +import java.util.Map; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class Counter extends ForwardingMap { + private final Map counts = new HashMap<>(); + + public long decrement(@Nullable T key) { + return increment(key, -1); + } + public long increment(@Nullable T key) { + return increment(key, 1); + } + public long decrement(@Nullable T key, long amount) { + return increment(key, -amount); + } + public long increment(@Nullable T key, long amount) { + Long count = this.getCount(key); + count += amount; + this.counts.put(key, count); + return count; + } + + public long getCount(@Nullable T key) { + return this.counts.getOrDefault(key, 0L); + } + + @NotNull + @Override + protected Map delegate() { + return this.counts; + } +} diff --git a/src/main/java/co/aikar/util/JSONUtil.java b/src/main/java/co/aikar/util/JSONUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..190bf0598442c89c2a1c93ad7c8c1a29797304ae --- /dev/null +++ b/src/main/java/co/aikar/util/JSONUtil.java @@ -0,0 +1,140 @@ +package co.aikar.util; + +import com.google.common.base.Function; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Provides Utility methods that assist with generating JSON Objects + */ +@SuppressWarnings({"rawtypes", "SuppressionAnnotation"}) +public final class JSONUtil { + private JSONUtil() {} + + /** + * Creates a key/value "JSONPair" object + * + * @param key Key to use + * @param obj Value to use + * @return JSONPair + */ + @NotNull + public static JSONPair pair(@NotNull String key, @Nullable Object obj) { + return new JSONPair(key, obj); + } + + @NotNull + public static JSONPair pair(long key, @Nullable Object obj) { + return new JSONPair(String.valueOf(key), obj); + } + + /** + * Creates a new JSON object from multiple JSONPair key/value pairs + * + * @param data JSONPairs + * @return Map + */ + @NotNull + public static Map createObject(@NotNull JSONPair... data) { + return appendObjectData(new LinkedHashMap(), data); + } + + /** + * This appends multiple key/value Obj pairs into a JSON Object + * + * @param parent Map to be appended to + * @param data Data to append + * @return Map + */ + @NotNull + public static Map appendObjectData(@NotNull Map parent, @NotNull JSONPair... data) { + for (JSONPair JSONPair : data) { + parent.put(JSONPair.key, JSONPair.val); + } + return parent; + } + + /** + * This builds a JSON array from a set of data + * + * @param data Data to build JSON array from + * @return List + */ + @NotNull + public static List toArray(@NotNull Object... data) { + return Lists.newArrayList(data); + } + + /** + * These help build a single JSON array using a mapper function + * + * @param collection Collection to apply to + * @param mapper Mapper to apply + * @param Element Type + * @return List + */ + @NotNull + public static List toArrayMapper(@NotNull E[] collection, @NotNull Function mapper) { + return toArrayMapper(Lists.newArrayList(collection), mapper); + } + + @NotNull + public static List toArrayMapper(@NotNull Iterable collection, @NotNull Function 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 Element Type + * @return Map + */ + @NotNull + public static Map toObjectMapper(@NotNull E[] collection, @NotNull Function mapper) { + return toObjectMapper(Lists.newArrayList(collection), mapper); + } + + @NotNull + public static Map toObjectMapper(@NotNull Iterable collection, @NotNull Function mapper) { + Map object = Maps.newLinkedHashMap(); + for (E e : collection) { + JSONPair JSONPair = mapper.apply(e); + if (JSONPair != null) { + object.put(JSONPair.key, JSONPair.val); + } + } + return object; + } + + /** + * Simply stores a key and a value, used internally by many methods below. + */ + @SuppressWarnings("PublicInnerClass") + public static class JSONPair { + final String key; + final Object val; + + JSONPair(@NotNull String key, @NotNull Object val) { + this.key = key; + this.val = val; + } + } +} diff --git a/src/main/java/co/aikar/util/LoadingIntMap.java b/src/main/java/co/aikar/util/LoadingIntMap.java new file mode 100644 index 0000000000000000000000000000000000000000..63a899c7dbdb69daa4876a2ce2a7dfb734b5af9d --- /dev/null +++ b/src/main/java/co/aikar/util/LoadingIntMap.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2015. Starlis LLC / dba Empire Minecraft + * + * This source code is proprietary software and must not be redistributed without Starlis LLC's approval + * + */ +package co.aikar.util; + + +import com.google.common.base.Function; +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * Allows you to pass a Loader function that when a key is accessed that doesn't exist, + * automatically loads the entry into the map by calling the loader Function. + * + * .get() Will only return null if the Loader can return null. + * + * You may pass any backing Map to use. + * + * This class is not thread safe and should be wrapped with Collections.synchronizedMap on the OUTSIDE of the LoadingMap if needed. + * + * Do not wrap the backing map with Collections.synchronizedMap. + * + * @param Value + */ +public class LoadingIntMap extends Int2ObjectOpenHashMap { + private final Function loader; + + public LoadingIntMap(@NotNull Function loader) { + super(); + this.loader = loader; + } + + public LoadingIntMap(int expectedSize, @NotNull Function loader) { + super(expectedSize); + this.loader = loader; + } + + public LoadingIntMap(int expectedSize, float loadFactor, @NotNull Function loader) { + super(expectedSize, loadFactor); + this.loader = loader; + } + + + @Nullable + @Override + public V get(int key) { + V res = super.get(key); + if (res == null) { + res = loader.apply(key); + if (res != null) { + put(key, res); + } + } + return res; + } + + /** + * Due to java stuff, you will need to cast it to (Function) for some cases + * + * @param Type + */ + public abstract static class Feeder implements Function { + @Nullable + @Override + public T apply(@Nullable Object input) { + return apply(); + } + + @Nullable + public abstract T apply(); + } +} diff --git a/src/main/java/co/aikar/util/LoadingMap.java b/src/main/java/co/aikar/util/LoadingMap.java new file mode 100644 index 0000000000000000000000000000000000000000..aedbb03321886cb267879d7994653e447b485f6a --- /dev/null +++ b/src/main/java/co/aikar/util/LoadingMap.java @@ -0,0 +1,368 @@ +/* + * This file is licensed under the MIT License (MIT). + * + * Copyright (c) 2014 Daniel Ennis + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package co.aikar.util; + +import com.google.common.base.Preconditions; +import java.lang.reflect.Constructor; +import java.util.AbstractMap; +import java.util.Collection; +import java.util.HashMap; +import java.util.IdentityHashMap; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * Allows you to pass a Loader function that when a key is accessed that doesn't exists, + * automatically loads the entry into the map by calling the loader Function. + * + * .get() Will only return null if the Loader can return null. + * + * You may pass any backing Map to use. + * + * This class is not thread safe and should be wrapped with Collections.synchronizedMap on the OUTSIDE of the LoadingMap if needed. + * + * Do not wrap the backing map with Collections.synchronizedMap. + * + * @param Key + * @param Value + */ +public class LoadingMap extends AbstractMap { + private final Map backingMap; + private final java.util.function.Function loader; + + /** + * Initializes an auto loading map using specified loader and backing map + * @param backingMap Map to wrap + * @param loader Loader + */ + public LoadingMap(@NotNull Map backingMap, @NotNull java.util.function.Function 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 Key Type of the Map + * @param Value Type of the Map + * @return Map + */ + @NotNull + public static Map of(@NotNull Map backingMap, @NotNull Function 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 Key Type of the Map + * @param Value Type of the Map + * @return Map that auto instantiates on .get() + */ + @NotNull + public static Map newAutoMap(@NotNull Map backingMap, @Nullable final Class keyClass, + @NotNull final Class 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 Key Type of the Map + * @param Value Type of the Map + * @return Map that auto instantiates on .get() + */ + @NotNull + public static Map newAutoMap(@NotNull Map backingMap, + @NotNull final Class 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 Key Type of the Map + * @param Value Type of the Map + * @return Map that auto instantiates on .get() + */ + @NotNull + public static Map newHashAutoMap(@Nullable final Class keyClass, @NotNull final Class 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 Key Type of the Map + * @param Value Type of the Map + * @return Map that auto instantiates on .get() + */ + @NotNull + public static Map newHashAutoMap(@NotNull final Class 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 Key Type of the Map + * @param Value Type of the Map + * @return Map that auto instantiates on .get() + */ + @NotNull + public static Map newHashAutoMap(@Nullable final Class keyClass, @NotNull final Class 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 Key Type of the Map + * @param Value Type of the Map + * @return Map that auto instantiates on .get() + */ + @NotNull + public static Map newHashAutoMap(@NotNull final Class 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 Key Type of the Map + * @param Value Type of the Map + * @return Map + */ + @NotNull + public static Map newHashMap(@NotNull Function 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 Key Type of the Map + * @param Value Type of the Map + * @return Map + */ + @NotNull + public static Map newHashMap(@NotNull Function 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 Key Type of the Map + * @param Value Type of the Map + * @return Map + */ + @NotNull + public static Map newHashMap(@NotNull Function 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 Key Type of the Map + * @param Value Type of the Map + * @return Map + */ + @NotNull + public static Map newIdentityHashMap(@NotNull Function 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 Key Type of the Map + * @param Value Type of the Map + * @return Map + */ + @NotNull + public static Map newIdentityHashMap(@NotNull Function loader, int initialCapacity) { + return new LoadingMap<>(new IdentityHashMap<>(initialCapacity), loader); + } + + @Override + public int size() {return backingMap.size();} + + @Override + public boolean isEmpty() {return backingMap.isEmpty();} + + @Override + public boolean containsKey(@Nullable Object key) {return backingMap.containsKey(key);} + + @Override + public boolean containsValue(@Nullable Object value) {return backingMap.containsValue(value);} + + @Nullable + @Override + public V get(@Nullable Object key) { + V v = backingMap.get(key); + if (v != null) { + return v; + } + return backingMap.computeIfAbsent((K) key, loader); + } + + @Nullable + public V put(@Nullable K key, @Nullable V value) {return backingMap.put(key, value);} + + @Nullable + @Override + public V remove(@Nullable Object key) {return backingMap.remove(key);} + + public void putAll(@NotNull Map m) {backingMap.putAll(m);} + + @Override + public void clear() {backingMap.clear();} + + @NotNull + @Override + public Set keySet() {return backingMap.keySet();} + + @NotNull + @Override + public Collection values() {return backingMap.values();} + + @Override + public boolean equals(@Nullable Object o) {return backingMap.equals(o);} + + @Override + public int hashCode() {return backingMap.hashCode();} + + @NotNull + @Override + public Set> entrySet() { + return backingMap.entrySet(); + } + + @NotNull + public LoadingMap clone() { + return new LoadingMap<>(backingMap, loader); + } + + private static class AutoInstantiatingLoader implements Function { + final Constructor constructor; + private final Class valueClass; + + AutoInstantiatingLoader(@Nullable Class keyClass, @NotNull Class valueClass) { + try { + this.valueClass = valueClass; + if (keyClass != null) { + constructor = valueClass.getConstructor(keyClass); + } else { + constructor = null; + } + } catch (NoSuchMethodException e) { + throw new IllegalStateException( + valueClass.getName() + " does not have a constructor for " + (keyClass != null ? keyClass.getName() : null)); + } + } + + @NotNull + @Override + public V apply(@Nullable K input) { + try { + return (constructor != null ? constructor.newInstance(input) : valueClass.newInstance()); + } catch (Exception e) { + throw new ExceptionInInitializerError(e); + } + } + + @Override + public int hashCode() { + return super.hashCode(); + } + + @Override + public boolean equals(Object object) { + return false; + } + } + + /** + * Due to java stuff, you will need to cast it to (Function) for some cases + * + * @param Type + */ + public abstract static class Feeder implements Function { + @Nullable + @Override + public T apply(@Nullable Object input) { + return apply(); + } + + @Nullable + public abstract T apply(); + } +} diff --git a/src/main/java/co/aikar/util/MRUMapCache.java b/src/main/java/co/aikar/util/MRUMapCache.java new file mode 100644 index 0000000000000000000000000000000000000000..5989ee21297935651b0edd44b8239e655eaef1d9 --- /dev/null +++ b/src/main/java/co/aikar/util/MRUMapCache.java @@ -0,0 +1,111 @@ +/* + * This file is licensed under the MIT License (MIT). + * + * Copyright (c) 2014 Daniel Ennis + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package co.aikar.util; + +import java.util.AbstractMap; +import java.util.Collection; +import java.util.Map; +import java.util.Set; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * Implements a Most Recently Used cache in front of a backing map, to quickly access the last accessed result. + * + * @param Key Type of the Map + * @param Value Type of the Map + */ +public class MRUMapCache extends AbstractMap { + final Map backingMap; + Object cacheKey; + V cacheValue; + public MRUMapCache(@NotNull final Map backingMap) { + this.backingMap = backingMap; + } + + public int size() {return backingMap.size();} + + public boolean isEmpty() {return backingMap.isEmpty();} + + public boolean containsKey(@Nullable Object key) { + return key != null && key.equals(cacheKey) || backingMap.containsKey(key); + } + + public boolean containsValue(@Nullable Object value) { + return value != null && value == cacheValue || backingMap.containsValue(value); + } + + @Nullable + public V get(@Nullable Object key) { + if (cacheKey != null && cacheKey.equals(key)) { + return cacheValue; + } + cacheKey = key; + return cacheValue = backingMap.get(key); + } + + @Nullable + public V put(@Nullable K key, @Nullable V value) { + cacheKey = key; + return cacheValue = backingMap.put(key, value); + } + + @Nullable + public V remove(@Nullable Object key) { + if (key != null && key.equals(cacheKey)) { + cacheKey = null; + } + return backingMap.remove(key); + } + + public void putAll(@NotNull Map m) {backingMap.putAll(m);} + + public void clear() { + cacheKey = null; + cacheValue = null; + backingMap.clear(); + } + + @NotNull + public Set keySet() {return backingMap.keySet();} + + @NotNull + public Collection values() {return backingMap.values();} + + @NotNull + public Set> entrySet() {return backingMap.entrySet();} + + /** + * Wraps the specified map with a most recently used cache + * + * @param map Map to be wrapped + * @param Key Type of the Map + * @param Value Type of the Map + * @return Map + */ + @NotNull + public static Map of(@NotNull Map map) { + return new MRUMapCache(map); + } +} diff --git a/src/main/java/org/bukkit/Bukkit.java b/src/main/java/org/bukkit/Bukkit.java index 16ec0cd908659d6d53e565281b8db9f81951277d..92b3c40959eac827ed8861fec8658e57f2910628 100644 --- a/src/main/java/org/bukkit/Bukkit.java +++ b/src/main/java/org/bukkit/Bukkit.java @@ -802,7 +802,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 0a333c1439b3623d38029b88ce6d5ff159ae1ddd..bce277cd3a452769cd86dc017a2e548a170d6acc 100644 --- a/src/main/java/org/bukkit/Server.java +++ b/src/main/java/org/bukkit/Server.java @@ -1768,6 +1768,26 @@ public interface Server extends PluginMessageRecipient, net.kyori.adventure.audi throw new UnsupportedOperationException("Not supported yet."); } + // Paper start + @NotNull + public org.bukkit.configuration.file.YamlConfiguration getBukkitConfig() + { + throw new UnsupportedOperationException( "Not supported yet." ); + } + + @NotNull + public org.bukkit.configuration.file.YamlConfiguration getSpigotConfig() + { + throw new UnsupportedOperationException("Not supported yet."); + } + + @NotNull + public org.bukkit.configuration.file.YamlConfiguration getPaperConfig() + { + throw new UnsupportedOperationException("Not supported yet."); + } + // Paper end + /** * Sends the component to the player * diff --git a/src/main/java/org/bukkit/UnsafeValues.java b/src/main/java/org/bukkit/UnsafeValues.java index dc9a4f12d9f05a3ae7c9f1c7648123e4b3dfd115..daf3ac72cae4d19c0273058dc6a1e1afe9a47f77 100644 --- a/src/main/java/org/bukkit/UnsafeValues.java +++ b/src/main/java/org/bukkit/UnsafeValues.java @@ -33,6 +33,7 @@ public interface UnsafeValues { net.kyori.adventure.text.Component resolveWithContext(net.kyori.adventure.text.Component component, org.bukkit.command.CommandSender context, org.bukkit.entity.Entity scoreboardSubject, boolean bypassPermissions) throws java.io.IOException; // Paper end + void reportTimings(); // Paper Material toLegacy(Material material); Material fromLegacy(Material material); @@ -106,4 +107,12 @@ public interface UnsafeValues { return !Bukkit.getUnsafe().isSupportedApiVersion(plugin.getDescription().getAPIVersion()); } // Paper end + + // Paper start + /** + * Server name to report to timings v2 + * @return name + */ + String getTimingsServerName(); + // Paper end } diff --git a/src/main/java/org/bukkit/command/BufferedCommandSender.java b/src/main/java/org/bukkit/command/BufferedCommandSender.java new file mode 100644 index 0000000000000000000000000000000000000000..f9a00aecca5ec41b460bf41dfe1c69694768cf98 --- /dev/null +++ b/src/main/java/org/bukkit/command/BufferedCommandSender.java @@ -0,0 +1,21 @@ +package org.bukkit.command; + +import org.jetbrains.annotations.NotNull; + +public class BufferedCommandSender implements MessageCommandSender { + private final StringBuffer buffer = new StringBuffer(); + @Override + public void sendMessage(@NotNull String message) { + buffer.append(message); + buffer.append("\n"); + } + + @NotNull + public String getBuffer() { + return buffer.toString(); + } + + public void reset() { + this.buffer.setLength(0); + } +} diff --git a/src/main/java/org/bukkit/command/Command.java b/src/main/java/org/bukkit/command/Command.java index 75759131bd94b672bec4cd8e271ebff1ad391cba..5cfd88eec9bf02e83d77b6fce1a5c14b7687f48b 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 net.kyori.adventure.text.Component permissionMessage; // Paper - public org.spigotmc.CustomTimingsHandler timings; // Spigot + public co.aikar.timings.Timing timings; // Paper + @NotNull public String getTimingName() {return getName();} // Paper protected Command(@NotNull String name) { this(name, "", "/" + name, new ArrayList()); @@ -47,7 +48,6 @@ public abstract class Command { this.usageMessage = (usageMessage == null) ? "/" + name : usageMessage; this.aliases = aliases; this.activeAliases = new ArrayList(aliases); - this.timings = new org.spigotmc.CustomTimingsHandler("** Command: " + name); // Spigot } /** @@ -245,7 +245,6 @@ public abstract class Command { } this.nextLabel = name; if (!isRegistered()) { - this.timings = new org.spigotmc.CustomTimingsHandler("** Command: " + name); // Spigot this.label = name; return true; } diff --git a/src/main/java/org/bukkit/command/FormattedCommandAlias.java b/src/main/java/org/bukkit/command/FormattedCommandAlias.java index d6c8938b1e13b63116b7b0e074ea8ef5997f8dc3..a6ad94ef98a1df1d2842635d850bc990b0137849 100644 --- a/src/main/java/org/bukkit/command/FormattedCommandAlias.java +++ b/src/main/java/org/bukkit/command/FormattedCommandAlias.java @@ -9,6 +9,7 @@ public class FormattedCommandAlias extends Command { public FormattedCommandAlias(@NotNull String alias, @NotNull String[] formatStrings) { super(alias); + timings = co.aikar.timings.TimingsManager.getCommandTiming("minecraft", this); // Spigot this.formatStrings = formatStrings; } @@ -113,6 +114,10 @@ public class FormattedCommandAlias extends Command { return formatString; } + @NotNull + @Override // Paper + public String getTimingName() {return "Command Forwarder - " + super.getTimingName();} // Paper + private static boolean inRange(int i, int j, int k) { return i >= j && i <= k; } diff --git a/src/main/java/org/bukkit/command/MessageCommandSender.java b/src/main/java/org/bukkit/command/MessageCommandSender.java new file mode 100644 index 0000000000000000000000000000000000000000..9d263ab3afb938c215c0b64d9171345fca6ceb2c --- /dev/null +++ b/src/main/java/org/bukkit/command/MessageCommandSender.java @@ -0,0 +1,135 @@ +package org.bukkit.command; + +import org.bukkit.Bukkit; +import org.bukkit.Server; +import org.bukkit.permissions.Permission; +import org.bukkit.permissions.PermissionAttachment; +import org.bukkit.permissions.PermissionAttachmentInfo; +import org.bukkit.plugin.Plugin; + +import java.util.Set; +import java.util.UUID; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * For when all you care about is just messaging + */ +public interface MessageCommandSender extends CommandSender { + + @Override + default void sendMessage(@NotNull String[] messages) { + for (String message : messages) { + sendMessage(message); + } + } + + @Override + default void sendMessage(@Nullable UUID sender, @NotNull String message) { + sendMessage(message); + } + + @Override + default void sendMessage(@Nullable UUID sender, @NotNull String[] messages) { + for (String message : messages) { + sendMessage(message); + } + } + + @NotNull + @Override + default Server getServer() { + return Bukkit.getServer(); + } + + // Paper start + @Override + default net.kyori.adventure.text.@org.jetbrains.annotations.NotNull Component name() { + throw new UnsupportedOperationException(); + } + // Paper end + + @NotNull + @Override + default String getName() { + throw new UnsupportedOperationException(); + } + + @Override + default boolean isOp() { + throw new UnsupportedOperationException(); + } + + @Override + default void setOp(boolean value) { + throw new UnsupportedOperationException(); + } + + @Override + default boolean isPermissionSet(@NotNull String name) { + throw new UnsupportedOperationException(); + } + + @Override + default boolean isPermissionSet(@NotNull Permission perm) { + throw new UnsupportedOperationException(); + } + + @Override + default boolean hasPermission(@NotNull String name) { + throw new UnsupportedOperationException(); + } + + @Override + default boolean hasPermission(@NotNull Permission perm) { + throw new UnsupportedOperationException(); + } + + @NotNull + @Override + default PermissionAttachment addAttachment(@NotNull Plugin plugin, @NotNull String name, boolean value) { + throw new UnsupportedOperationException(); + } + + @NotNull + @Override + default PermissionAttachment addAttachment(@NotNull Plugin plugin) { + throw new UnsupportedOperationException(); + } + + @NotNull + @Override + default PermissionAttachment addAttachment(@NotNull Plugin plugin, @NotNull String name, boolean value, int ticks) { + throw new UnsupportedOperationException(); + } + + @NotNull + @Override + default PermissionAttachment addAttachment(@NotNull Plugin plugin, int ticks) { + throw new UnsupportedOperationException(); + } + + @Override + default void removeAttachment(@NotNull PermissionAttachment attachment) { + throw new UnsupportedOperationException(); + } + + @Override + default void recalculatePermissions() { + throw new UnsupportedOperationException(); + } + + @NotNull + @Override + default Set getEffectivePermissions() { + throw new UnsupportedOperationException(); + } + + @NotNull + @Override + default Spigot spigot() { + throw new UnsupportedOperationException(); + } + +} diff --git a/src/main/java/org/bukkit/command/SimpleCommandMap.java b/src/main/java/org/bukkit/command/SimpleCommandMap.java index 53f28c9e6843991486a576d41b6641c170589807..4205649948a9e2a72f64c3f007112245abac6d50 100644 --- a/src/main/java/org/bukkit/command/SimpleCommandMap.java +++ b/src/main/java/org/bukkit/command/SimpleCommandMap.java @@ -15,7 +15,6 @@ import org.bukkit.command.defaults.BukkitCommand; import org.bukkit.command.defaults.HelpCommand; import org.bukkit.command.defaults.PluginsCommand; import org.bukkit.command.defaults.ReloadCommand; -import org.bukkit.command.defaults.TimingsCommand; import org.bukkit.command.defaults.VersionCommand; import org.bukkit.entity.Player; import org.bukkit.util.StringUtil; @@ -35,7 +34,7 @@ public class SimpleCommandMap implements CommandMap { register("bukkit", new VersionCommand("version")); register("bukkit", new ReloadCommand("reload")); //register("bukkit", new PluginsCommand("plugins")); // Paper - register("bukkit", new TimingsCommand("timings")); + register("bukkit", new co.aikar.timings.TimingsCommand("timings")); // Paper } public void setFallbackCommands() { @@ -67,6 +66,7 @@ public class SimpleCommandMap implements CommandMap { */ @Override public boolean register(@NotNull String label, @NotNull String fallbackPrefix, @NotNull Command command) { + command.timings = co.aikar.timings.TimingsManager.getCommandTiming(fallbackPrefix, command); // Paper label = label.toLowerCase(java.util.Locale.ENGLISH).trim(); fallbackPrefix = fallbackPrefix.toLowerCase(java.util.Locale.ENGLISH).trim(); boolean registered = register(label, command, false, fallbackPrefix); @@ -143,16 +143,22 @@ public class SimpleCommandMap implements CommandMap { return false; } + // Paper start - Plugins do weird things to workaround normal registration + if (target.timings == null) { + target.timings = co.aikar.timings.TimingsManager.getCommandTiming(null, target); + } + // Paper end + try { - target.timings.startTiming(); // Spigot + try (co.aikar.timings.Timing ignored = target.timings.startTiming()) { // Paper - use try with resources // Note: we don't return the result of target.execute as thats success / failure, we return handled (true) or not handled (false) target.execute(sender, sentCommandLabel, Arrays.copyOfRange(args, 1, args.length)); - target.timings.stopTiming(); // Spigot + } // target.timings.stopTiming(); // Spigot // Paper } catch (CommandException ex) { - target.timings.stopTiming(); // Spigot + //target.timings.stopTiming(); // Spigot // Paper throw ex; } catch (Throwable ex) { - target.timings.stopTiming(); // Spigot + //target.timings.stopTiming(); // Spigot // Paper throw new CommandException("Unhandled exception executing '" + commandLine + "' in " + target, ex); } diff --git a/src/main/java/org/bukkit/command/defaults/TimingsCommand.java b/src/main/java/org/bukkit/command/defaults/TimingsCommand.java deleted file mode 100644 index 516d7fc7812aac343782861d0d567f54aa578c2a..0000000000000000000000000000000000000000 --- a/src/main/java/org/bukkit/command/defaults/TimingsCommand.java +++ /dev/null @@ -1,250 +0,0 @@ -package org.bukkit.command.defaults; - -import com.google.common.base.Preconditions; -import com.google.common.collect.ImmutableList; -import java.io.File; -import java.io.IOException; -import java.io.PrintStream; -import java.util.ArrayList; -import java.util.List; -import org.bukkit.Bukkit; -import org.bukkit.ChatColor; -import org.bukkit.command.CommandSender; -import org.bukkit.event.Event; -import org.bukkit.event.HandlerList; -import org.bukkit.plugin.Plugin; -import org.bukkit.plugin.RegisteredListener; -import org.bukkit.plugin.TimedRegisteredListener; -import org.bukkit.util.StringUtil; -import org.jetbrains.annotations.NotNull; - -// Spigot start -// CHECKSTYLE:OFF -import java.io.ByteArrayOutputStream; -import java.io.OutputStream; -import java.net.HttpURLConnection; -import java.net.URL; -import java.util.logging.Level; -import org.bukkit.command.RemoteConsoleCommandSender; -import org.bukkit.plugin.SimplePluginManager; -import org.spigotmc.CustomTimingsHandler; -// CHECKSTYLE:ON -// Spigot end - -public class TimingsCommand extends BukkitCommand { - private static final List TIMINGS_SUBCOMMANDS = ImmutableList.of("report", "reset", "on", "off", "paste"); // Spigot - public static long timingStart = 0; // Spigot - - public TimingsCommand(@NotNull String name) { - super(name); - this.description = "Manages Spigot Timings data to see performance of the server."; // Spigot - this.usageMessage = "/timings "; // Spigot - this.setPermission("bukkit.command.timings"); - } - - // Spigot start - redesigned Timings Command - public void executeSpigotTimings(@NotNull CommandSender sender, @NotNull String[] args) { - if ("on".equals(args[0])) { - ((SimplePluginManager) Bukkit.getPluginManager()).useTimings(true); - CustomTimingsHandler.reload(); - sender.sendMessage("Enabled Timings & Reset"); - return; - } else if ("off".equals(args[0])) { - ((SimplePluginManager) Bukkit.getPluginManager()).useTimings(false); - sender.sendMessage("Disabled Timings"); - return; - } - - if (!Bukkit.getPluginManager().useTimings()) { - sender.sendMessage("Please enable timings by typing /timings on"); - return; - } - - boolean paste = "paste".equals(args[0]); - if ("reset".equals(args[0])) { - CustomTimingsHandler.reload(); - sender.sendMessage("Timings reset"); - } else if ("merged".equals(args[0]) || "report".equals(args[0]) || paste) { - long sampleTime = System.nanoTime() - timingStart; - int index = 0; - File timingFolder = new File("timings"); - timingFolder.mkdirs(); - File timings = new File(timingFolder, "timings.txt"); - ByteArrayOutputStream bout = (paste) ? new ByteArrayOutputStream() : null; - while (timings.exists()) timings = new File(timingFolder, "timings" + (++index) + ".txt"); - PrintStream fileTimings = null; - try { - fileTimings = (paste) ? new PrintStream(bout) : new PrintStream(timings); - - CustomTimingsHandler.printTimings(fileTimings); - fileTimings.println("Sample time " + sampleTime + " (" + sampleTime / 1E9 + "s)"); - - fileTimings.println(""); - fileTimings.println(Bukkit.spigot().getConfig().saveToString()); - fileTimings.println(""); - - if (paste) { - new PasteThread(sender, bout).start(); - return; - } - - sender.sendMessage("Timings written to " + timings.getPath()); - sender.sendMessage("Paste contents of file into form at http://www.spigotmc.org/go/timings to read results."); - - } catch (IOException e) { - } finally { - if (fileTimings != null) { - fileTimings.close(); - } - } - } - } - // Spigot end - - @Override - public boolean execute(@NotNull CommandSender sender, @NotNull String currentAlias, @NotNull String[] args) { - if (!testPermission(sender)) return true; - if (args.length < 1) { // Spigot - sender.sendMessage(ChatColor.RED + "Usage: " + usageMessage); - return false; - } - // Spigot start - if (true) { - executeSpigotTimings(sender, args); - return true; - } - // Spigot end - if (!sender.getServer().getPluginManager().useTimings()) { - sender.sendMessage("Please enable timings by setting \"settings.plugin-profiling\" to true in bukkit.yml"); - return true; - } - - boolean separate = "separate".equalsIgnoreCase(args[0]); - if ("reset".equalsIgnoreCase(args[0])) { - for (HandlerList handlerList : HandlerList.getHandlerLists()) { - for (RegisteredListener listener : handlerList.getRegisteredListeners()) { - if (listener instanceof TimedRegisteredListener) { - ((TimedRegisteredListener) listener).reset(); - } - } - } - sender.sendMessage("Timings reset"); - } else if ("merged".equalsIgnoreCase(args[0]) || separate) { - - int index = 0; - int pluginIdx = 0; - File timingFolder = new File("timings"); - timingFolder.mkdirs(); - File timings = new File(timingFolder, "timings.txt"); - File names = null; - while (timings.exists()) timings = new File(timingFolder, "timings" + (++index) + ".txt"); - PrintStream fileTimings = null; - PrintStream fileNames = null; - try { - fileTimings = new PrintStream(timings); - if (separate) { - names = new File(timingFolder, "names" + index + ".txt"); - fileNames = new PrintStream(names); - } - for (Plugin plugin : Bukkit.getPluginManager().getPlugins()) { - pluginIdx++; - long totalTime = 0; - if (separate) { - fileNames.println(pluginIdx + " " + plugin.getDescription().getFullName()); - fileTimings.println("Plugin " + pluginIdx); - } else { - fileTimings.println(plugin.getDescription().getFullName()); - } - for (RegisteredListener listener : HandlerList.getRegisteredListeners(plugin)) { - if (listener instanceof TimedRegisteredListener) { - TimedRegisteredListener trl = (TimedRegisteredListener) listener; - long time = trl.getTotalTime(); - int count = trl.getCount(); - if (count == 0) continue; - long avg = time / count; - totalTime += time; - Class eventClass = trl.getEventClass(); - if (count > 0 && eventClass != null) { - fileTimings.println(" " + eventClass.getSimpleName() + (trl.hasMultiple() ? " (and sub-classes)" : "") + " Time: " + time + " Count: " + count + " Avg: " + avg); - } - } - } - fileTimings.println(" Total time " + totalTime + " (" + totalTime / 1000000000 + "s)"); - } - sender.sendMessage("Timings written to " + timings.getPath()); - if (separate) sender.sendMessage("Names written to " + names.getPath()); - } catch (IOException e) { - } finally { - if (fileTimings != null) { - fileTimings.close(); - } - if (fileNames != null) { - fileNames.close(); - } - } - } else { - sender.sendMessage(ChatColor.RED + "Usage: " + usageMessage); - return false; - } - return true; - } - - @NotNull - @Override - public List tabComplete(@NotNull CommandSender sender, @NotNull String alias, @NotNull String[] args) { - Preconditions.checkArgument(sender != null, "Sender cannot be null"); - Preconditions.checkArgument(args != null, "Arguments cannot be null"); - Preconditions.checkArgument(alias != null, "Alias cannot be null"); - - if (args.length == 1) { - return StringUtil.copyPartialMatches(args[0], TIMINGS_SUBCOMMANDS, new ArrayList(TIMINGS_SUBCOMMANDS.size())); - } - return ImmutableList.of(); - } - - // Spigot start - private static class PasteThread extends Thread { - - private final CommandSender sender; - private final ByteArrayOutputStream bout; - - public PasteThread(@NotNull CommandSender sender, @NotNull ByteArrayOutputStream bout) { - super("Timings paste thread"); - this.sender = sender; - this.bout = bout; - } - - @Override - public synchronized void start() { - if (sender instanceof RemoteConsoleCommandSender) { - run(); - } else { - super.start(); - } - } - - @Override - public void run() { - try { - HttpURLConnection con = (HttpURLConnection) new URL("https://timings.spigotmc.org/paste").openConnection(); - con.setDoOutput(true); - con.setRequestMethod("POST"); - con.setInstanceFollowRedirects(false); - - OutputStream out = con.getOutputStream(); - out.write(bout.toByteArray()); - out.close(); - - com.google.gson.JsonObject location = new com.google.gson.Gson().fromJson(new java.io.InputStreamReader(con.getInputStream()), com.google.gson.JsonObject.class); - con.getInputStream().close(); - - String pasteID = location.get("key").getAsString(); - sender.sendMessage(ChatColor.GREEN + "Timings results can be viewed at https://www.spigotmc.org/go/timings?url=" + pasteID); - } catch (IOException ex) { - sender.sendMessage(ChatColor.RED + "Error pasting timings, check your console for more information"); - Bukkit.getServer().getLogger().log(Level.WARNING, "Could not paste timings", ex); - } - } - } - // Spigot end -} diff --git a/src/main/java/org/bukkit/entity/Player.java b/src/main/java/org/bukkit/entity/Player.java index fbf56bcc4540f7f1a8ced7cc2cd96e291c1ac273..8602b96bfc860ff1fea4c1c7a3655ce3a9516cf4 100644 --- a/src/main/java/org/bukkit/entity/Player.java +++ b/src/main/java/org/bukkit/entity/Player.java @@ -2077,7 +2077,14 @@ public interface Player extends HumanEntity, Conversable, OfflinePlayer, PluginM @Deprecated // Paper public void sendMessage(@NotNull net.md_5.bungee.api.ChatMessageType position, @Nullable java.util.UUID sender, @NotNull net.md_5.bungee.api.chat.BaseComponent... components) { throw new UnsupportedOperationException("Not supported yet."); + } + + // Paper start + public int getPing() { + throw new UnsupportedOperationException( "Not supported yet." ); + } + // Paper end } @NotNull diff --git a/src/main/java/org/bukkit/plugin/SimplePluginManager.java b/src/main/java/org/bukkit/plugin/SimplePluginManager.java index f64a90241ce7f73e111e6b5ac6ff87cd93583c0e..f13dcbe448111b24b36105d25527ba87ccc9334e 100644 --- a/src/main/java/org/bukkit/plugin/SimplePluginManager.java +++ b/src/main/java/org/bukkit/plugin/SimplePluginManager.java @@ -368,7 +368,6 @@ public final class SimplePluginManager implements PluginManager { } } - org.bukkit.command.defaults.TimingsCommand.timingStart = System.nanoTime(); // Spigot return result.toArray(new Plugin[result.size()]); } @@ -416,9 +415,9 @@ public final class SimplePluginManager implements PluginManager { if (result != null) { plugins.add(result); - lookupNames.put(result.getDescription().getName(), result); + lookupNames.put(result.getDescription().getName().toLowerCase(java.util.Locale.ENGLISH), result); // Paper for (String provided : result.getDescription().getProvides()) { - lookupNames.putIfAbsent(provided, result); + lookupNames.putIfAbsent(provided.toLowerCase(java.util.Locale.ENGLISH), result); // Paper } } @@ -448,7 +447,7 @@ public final class SimplePluginManager implements PluginManager { @Nullable public synchronized Plugin getPlugin(@NotNull String name) { if (true) {return this.paperPluginManager.getPlugin(name);} // Paper - return lookupNames.get(name.replace(' ', '_')); + return lookupNames.get(name.replace(' ', '_').toLowerCase(java.util.Locale.ENGLISH)); // Paper } @Override @@ -676,7 +675,8 @@ public final class SimplePluginManager implements PluginManager { throw new IllegalPluginAccessException("Plugin attempted to register " + event + " while not enabled"); } - if (useTimings) { + executor = new co.aikar.timings.TimedEventExecutor(executor, plugin, null, event); // Paper + if (false) { // Spigot - RL handles useTimings check now // Paper getEventListeners(event).register(new TimedRegisteredListener(listener, executor, priority, plugin, ignoreCancelled)); } else { getEventListeners(event).register(new RegisteredListener(listener, executor, priority, plugin, ignoreCancelled)); @@ -911,7 +911,7 @@ public final class SimplePluginManager implements PluginManager { @Override public boolean useTimings() { if (true) {return this.paperPluginManager.useTimings();} // Paper - return useTimings; + return co.aikar.timings.Timings.isTimingsEnabled(); // Spigot } /** @@ -920,7 +920,7 @@ public final class SimplePluginManager implements PluginManager { * @param use True if per event timing code should be used */ public void useTimings(boolean use) { - useTimings = use; + co.aikar.timings.Timings.setTimingsEnabled(use); // Paper } // Paper start diff --git a/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java b/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java index ab04ffe4cd05315a2ee0f64c553b4c674740eb7f..de6b6188587fd254ab96cbc1f95fda2056d9cbe2 100644 --- a/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java +++ b/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java @@ -55,7 +55,6 @@ public final class JavaPluginLoader implements PluginLoader { private final Pattern[] fileFilters = new Pattern[]{Pattern.compile("\\.jar$")}; private final List loaders = new CopyOnWriteArrayList(); private final LibraryLoader libraryLoader; - public static final CustomTimingsHandler pluginParentTimer = new CustomTimingsHandler("** Plugins"); // Spigot /** * This class was not meant to be constructed explicitly @@ -294,27 +293,21 @@ public final class JavaPluginLoader implements PluginLoader { } } - final CustomTimingsHandler timings = new CustomTimingsHandler("Plugin: " + plugin.getDescription().getFullName() + " Event: " + listener.getClass().getName() + "::" + method.getName() + "(" + eventClass.getSimpleName() + ")", pluginParentTimer); // Spigot - EventExecutor executor = new EventExecutor() { + EventExecutor executor = new co.aikar.timings.TimedEventExecutor(new EventExecutor() { // Paper @Override - public void execute(@NotNull Listener listener, @NotNull Event event) throws EventException { + public void execute(@NotNull Listener listener, @NotNull Event event) throws EventException { // Paper try { if (!eventClass.isAssignableFrom(event.getClass())) { return; } - // Spigot start - boolean isAsync = event.isAsynchronous(); - if (!isAsync) timings.startTiming(); method.invoke(listener, event); - if (!isAsync) timings.stopTiming(); - // Spigot end } catch (InvocationTargetException ex) { throw new EventException(ex.getCause()); } catch (Throwable t) { throw new EventException(t); } } - }; + }, plugin, method, eventClass); // Paper if (false) { // Spigot - RL handles useTimings check now eventSet.add(new TimedRegisteredListener(listener, executor, eh.priority(), plugin, eh.ignoreCancelled())); } else { diff --git a/src/main/java/org/bukkit/util/CachedServerIcon.java b/src/main/java/org/bukkit/util/CachedServerIcon.java index 5ca863b3692b2e1b58e7fb4d82f554a92cc4f01e..612958a331575d1da2715531ebdf6b1168f2e860 100644 --- a/src/main/java/org/bukkit/util/CachedServerIcon.java +++ b/src/main/java/org/bukkit/util/CachedServerIcon.java @@ -2,6 +2,7 @@ package org.bukkit.util; import org.bukkit.Server; import org.bukkit.event.server.ServerListPingEvent; +import org.jetbrains.annotations.Nullable; /** * This is a cached version of a server-icon. It's internal representation @@ -12,4 +13,9 @@ import org.bukkit.event.server.ServerListPingEvent; * @see Server#loadServerIcon(java.io.File) * @see ServerListPingEvent#setServerIcon(CachedServerIcon) */ -public interface CachedServerIcon {} +public interface CachedServerIcon { + + @Nullable + public String getData(); // Paper + +} diff --git a/src/main/java/org/spigotmc/CustomTimingsHandler.java b/src/main/java/org/spigotmc/CustomTimingsHandler.java index 44badfedcc3fdc26bdc293b85d8c781d6f659faa..123647bb10fc89508437d7a0bd3fd31d58ee7c82 100644 --- a/src/main/java/org/spigotmc/CustomTimingsHandler.java +++ b/src/main/java/org/spigotmc/CustomTimingsHandler.java @@ -1,137 +1,67 @@ +/* + * This file is licensed under the MIT License (MIT). + * + * Copyright (c) 2014 Daniel Ennis + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ package org.spigotmc; -import java.io.PrintStream; -import java.util.Queue; -import java.util.concurrent.ConcurrentLinkedQueue; import org.bukkit.Bukkit; -import org.bukkit.World; -import org.bukkit.command.defaults.TimingsCommand; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; +import org.bukkit.plugin.AuthorNagException; +import co.aikar.timings.Timing; +import co.aikar.timings.Timings; +import co.aikar.timings.TimingsManager; + +import java.lang.reflect.Method; +import java.util.logging.Level; /** - * Provides custom timing sections for /timings merged. + * This is here for legacy purposes incase any plugin used it. + * + * If you use this, migrate ASAP as this will be removed in the future! + * + * @deprecated + * @see co.aikar.timings.Timings#of */ -public class CustomTimingsHandler { - - private static Queue HANDLERS = new ConcurrentLinkedQueue(); - /*========================================================================*/ - private final String name; - private final CustomTimingsHandler parent; - private long count = 0; - private long start = 0; - private long timingDepth = 0; - private long totalTime = 0; - private long curTickTotal = 0; - private long violations = 0; +@Deprecated +public final class CustomTimingsHandler { + private final Timing handler; public CustomTimingsHandler(@NotNull String name) { - this(name, null); - } + Timing timing; - public CustomTimingsHandler(@NotNull String name, @Nullable CustomTimingsHandler parent) { - this.name = name; - this.parent = parent; - HANDLERS.add(this); - } - - /** - * Prints the timings and extra data to the given stream. - * - * @param printStream output stream - */ - public static void printTimings(@NotNull PrintStream printStream) { - printStream.println("Minecraft"); - for (CustomTimingsHandler timings : HANDLERS) { - long time = timings.totalTime; - long count = timings.count; - if (count == 0) { - continue; - } - long avg = time / count; - - printStream.println(" " + timings.name + " Time: " + time + " Count: " + count + " Avg: " + avg + " Violations: " + timings.violations); + new AuthorNagException("Deprecated use of CustomTimingsHandler. Please Switch to Timings.of ASAP").printStackTrace(); + 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; } - 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); + handler = timing; } - /** - * Resets all timings. - */ - public static void reload() { - if (Bukkit.getPluginManager().useTimings()) { - for (CustomTimingsHandler timings : HANDLERS) { - timings.reset(); - } - } - TimingsCommand.timingStart = System.nanoTime(); - } + public void startTiming() { handler.startTiming(); } + public void stopTiming() { handler.stopTiming(); } - /** - * 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 - } - } - } - - /** - * 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; - } - } - } - - /** - * 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(); - } - } - } - - /** - * Reset this timer, setting all values to zero. - */ - public void reset() { - count = 0; - violations = 0; - curTickTotal = 0; - totalTime = 0; - start = 0; - timingDepth = 0; - } }