From 64d9488334e3b3e421967247c570baeaff171002 Mon Sep 17 00:00:00 2001 From: Aikar Date: Mon, 29 Feb 2016 18:48:17 -0600 Subject: [PATCH] Timings v2 diff --git a/src/main/java/co/aikar/timings/FullServerTickHandler.java b/src/main/java/co/aikar/timings/FullServerTickHandler.java new file mode 100644 index 000000000..64531fcce --- /dev/null +++ b/src/main/java/co/aikar/timings/FullServerTickHandler.java @@ -0,0 +1,84 @@ +package co.aikar.timings; + +import static co.aikar.timings.TimingsManager.*; + +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(); + } + TimingsExport.reportTimings(); + } + + boolean isViolated() { + return record.getCurTickTotal() > 50000000; + } +} diff --git a/src/main/java/co/aikar/timings/NullTimingHandler.java b/src/main/java/co/aikar/timings/NullTimingHandler.java new file mode 100644 index 000000000..9b45ce887 --- /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 000000000..933ecf9bd --- /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.getName(), (verbose ? "## " : "") + + "Event: " + id + " (" + eventName + ")", null); + } + + @Override + public void execute(@NotNull Listener listener, @NotNull Event event) throws EventException { + if (event.isAsynchronous() || !Timings.timingsEnabled || !Bukkit.isPrimaryThread()) { + executor.execute(listener, event); + return; + } + try (Timing ignored = timings.startTiming()){ + executor.execute(listener, event); + } + } +} diff --git a/src/main/java/co/aikar/timings/Timing.java b/src/main/java/co/aikar/timings/Timing.java new file mode 100644 index 000000000..a21e5ead5 --- /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 000000000..a5d13a1e4 --- /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 000000000..cc0390c06 --- /dev/null +++ b/src/main/java/co/aikar/timings/TimingHandler.java @@ -0,0 +1,227 @@ +/* + * 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; + String reportTo; + if ("Minecraft".equalsIgnoreCase(last.identifier.group)) { + reportTo = "Paper! This is a potential bug in Paper"; + } else { + reportTo = "the plugin " + last.identifier.group + "(Look for errors above this in the logs)"; + } + Logger.getGlobal().log(Level.SEVERE, "TIMING_STACK_CORRUPTION - Report this to " + reportTo + " (" + 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 000000000..ddaed8127 --- /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 000000000..86d5ac6bd --- /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 000000000..df142a89b --- /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 000000000..0b34e0d01 --- /dev/null +++ b/src/main/java/co/aikar/timings/Timings.java @@ -0,0 +1,293 @@ +/* + * This file is licensed under the MIT License (MIT). + * + * Copyright (c) 2014 Daniel Ennis + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package co.aikar.timings; + +import com.google.common.base.Preconditions; +import com.google.common.collect.EvictingQueue; +import org.apache.commons.lang.Validate; +import org.bukkit.Bukkit; +import org.bukkit.command.CommandSender; +import org.bukkit.plugin.Plugin; + +import java.util.Queue; +import java.util.logging.Level; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +@SuppressWarnings({"UnusedDeclaration", "WeakerAccess", "SameParameterValue"}) +public final class Timings { + + private static final int MAX_HISTORY_FRAMES = 12; + public static final Timing NULL_HANDLER = new NullTimingHandler(); + static boolean timingsEnabled = false; + static boolean verboseEnabled = false; + private static int historyInterval = -1; + private static int historyLength = -1; + + private Timings() {} + + /** + * Returns a Timing for a plugin corresponding to a name. + * + * @param plugin Plugin to own the Timing + * @param name Name of Timing + * @return Handler + */ + @NotNull + public static Timing of(@NotNull Plugin plugin, @NotNull String name) { + Timing pluginHandler = null; + if (plugin != null) { + pluginHandler = ofSafe(plugin.getName(), "Combined Total", TimingsManager.PLUGIN_GROUP_HANDLER); + } + return of(plugin, name, pluginHandler); + } + + /** + *

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(); + } + TimingsExport.requestingReport.add(sender); + } + + /** + * Generates a report and sends it to the specified listener. + * Use with {@link org.bukkit.command.BufferedCommandSender} to get full response when done! + * @param sender The listener to send responses too. + */ + public static void generateReport(@NotNull TimingsReportListener sender) { + Validate.notNull(sender); + TimingsExport.requestingReport.add(sender); + } + + /* + ================= + Protected API: These are for internal use only in Bukkit/CraftBukkit + These do not have isPrimaryThread() checks in the startTiming/stopTiming + ================= + */ + @NotNull + static TimingHandler ofSafe(@NotNull String name) { + return ofSafe(null, name, null); + } + + @NotNull + static Timing ofSafe(@Nullable Plugin plugin, @NotNull String name) { + Timing pluginHandler = null; + if (plugin != null) { + pluginHandler = ofSafe(plugin.getName(), "Combined Total", TimingsManager.PLUGIN_GROUP_HANDLER); + } + return ofSafe(plugin != null ? plugin.getName() : "Minecraft - Invalid Plugin", name, pluginHandler); + } + + @NotNull + static TimingHandler ofSafe(@NotNull String name, @Nullable Timing groupHandler) { + return ofSafe(null, name, groupHandler); + } + + @NotNull + static TimingHandler ofSafe(@Nullable String groupName, @NotNull String name, @Nullable Timing groupHandler) { + return TimingsManager.getHandler(groupName, name, groupHandler); + } +} diff --git a/src/main/java/co/aikar/timings/TimingsCommand.java b/src/main/java/co/aikar/timings/TimingsCommand.java new file mode 100644 index 000000000..c0d8f2016 --- /dev/null +++ b/src/main/java/co/aikar/timings/TimingsCommand.java @@ -0,0 +1,122 @@ +/* + * This file is licensed under the MIT License (MIT). + * + * Copyright (c) 2014 Daniel Ennis + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package co.aikar.timings; + +import com.google.common.collect.ImmutableList; +import org.apache.commons.lang.Validate; +import org.bukkit.ChatColor; +import org.bukkit.command.CommandSender; +import org.bukkit.command.defaults.BukkitCommand; +import org.bukkit.util.StringUtil; + +import java.util.ArrayList; +import java.util.List; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + + +public class TimingsCommand extends BukkitCommand { + private static final List 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(ChatColor.RED + "Usage: " + usageMessage); + return true; + } + final String arg = args[0]; + if ("on".equalsIgnoreCase(arg)) { + Timings.setTimingsEnabled(true); + sender.sendMessage("Enabled Timings & Reset"); + return true; + } else if ("off".equalsIgnoreCase(arg)) { + Timings.setTimingsEnabled(false); + sender.sendMessage("Disabled Timings"); + return true; + } + + if (!Timings.isTimingsEnabled()) { + sender.sendMessage("Please enable timings by typing /timings on"); + return true; + } + + long now = System.currentTimeMillis(); + if ("verbon".equalsIgnoreCase(arg)) { + Timings.setVerboseTimingsEnabled(true); + sender.sendMessage("Enabled Verbose Timings"); + return true; + } else if ("verboff".equalsIgnoreCase(arg)) { + Timings.setVerboseTimingsEnabled(false); + sender.sendMessage("Disabled Verbose Timings"); + return true; + } else if ("reset".equalsIgnoreCase(arg)) { + if (now - lastResetAttempt < 30000) { + TimingsManager.reset(); + sender.sendMessage(ChatColor.RED + "Timings reset. Please wait 5-10 minutes before using /timings report."); + } else { + lastResetAttempt = now; + sender.sendMessage(ChatColor.RED + "WARNING: Timings v2 should not be reset. If you are encountering lag, please wait 3 minutes and then issue a report. The best timings will include 10+ minutes, with data before and after your lag period. If you really want to reset, run this command again within 30 seconds."); + } + + } else if ("cost".equals(arg)) { + sender.sendMessage("Timings cost: " + TimingsExport.getCost()); + } else if ( + "paste".equalsIgnoreCase(arg) || + "report".equalsIgnoreCase(arg) || + "get".equalsIgnoreCase(arg) || + "merged".equalsIgnoreCase(arg) || + "separate".equalsIgnoreCase(arg) + ) { + Timings.generateReport(sender); + } else { + sender.sendMessage(ChatColor.RED + "Usage: " + usageMessage); + } + return true; + } + + @NotNull + @Override + public List tabComplete(@NotNull CommandSender sender, @NotNull String alias, @NotNull String[] args) { + Validate.notNull(sender, "Sender cannot be null"); + Validate.notNull(args, "Arguments cannot be null"); + Validate.notNull(alias, "Alias cannot be null"); + + if (args.length == 1) { + return StringUtil.copyPartialMatches(args[0], TIMINGS_SUBCOMMANDS, + new ArrayList(TIMINGS_SUBCOMMANDS.size())); + } + return ImmutableList.of(); + } +} diff --git a/src/main/java/co/aikar/timings/TimingsExport.java b/src/main/java/co/aikar/timings/TimingsExport.java new file mode 100644 index 000000000..5923adfe6 --- /dev/null +++ b/src/main/java/co/aikar/timings/TimingsExport.java @@ -0,0 +1,355 @@ +/* + * This file is licensed under the MIT License (MIT). + * + * Copyright (c) 2014 Daniel Ennis + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package co.aikar.timings; + +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; +import org.apache.commons.lang.StringUtils; +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.Material; +import org.bukkit.command.CommandSender; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.MemorySection; +import org.bukkit.entity.EntityType; +import org.json.simple.JSONObject; +import org.json.simple.JSONValue; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.management.ManagementFactory; +import java.lang.management.RuntimeMXBean; +import java.net.HttpURLConnection; +import java.net.InetAddress; +import java.net.URL; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.logging.Level; +import java.util.zip.GZIPOutputStream; + +import static co.aikar.timings.TimingsManager.HISTORY; +import static co.aikar.util.JSONUtil.appendObjectData; +import static co.aikar.util.JSONUtil.createObject; +import static co.aikar.util.JSONUtil.pair; +import static co.aikar.util.JSONUtil.toArray; +import static co.aikar.util.JSONUtil.toArrayMapper; +import static co.aikar.util.JSONUtil.toObjectMapper; + +@SuppressWarnings({"rawtypes", "SuppressionAnnotation"}) +class TimingsExport extends Thread { + + private final TimingsReportListener listeners; + private final Map out; + private final TimingHistory[] history; + private static long lastReport = 0; + final static List requestingReport = Lists.newArrayList(); + + private TimingsExport(TimingsReportListener listeners, Map out, TimingHistory[] history) { + super("Timings paste thread"); + this.listeners = listeners; + this.out = out; + this.history = history; + } + + /** + * Checks if any pending reports are being requested, and builds one if needed. + */ + static void reportTimings() { + if (requestingReport.isEmpty()) { + return; + } + TimingsReportListener listeners = new TimingsReportListener(requestingReport); + listeners.addConsoleIfNeeded(); + + requestingReport.clear(); + long now = System.currentTimeMillis(); + final long lastReportDiff = now - lastReport; + if (lastReportDiff < 60000) { + listeners.sendMessage(ChatColor.RED + "Please wait at least 1 minute in between Timings reports. (" + (int)((60000 - lastReportDiff) / 1000) + " seconds)"); + listeners.done(); + return; + } + final long lastStartDiff = now - TimingsManager.timingStart; + if (lastStartDiff < 180000) { + listeners.sendMessage(ChatColor.RED + "Please wait at least 3 minutes before generating a Timings report. Unlike Timings v1, v2 benefits from longer timings and is not as useful with short timings. (" + (int)((180000 - lastStartDiff) / 1000) + " seconds)"); + listeners.done(); + return; + } + listeners.sendMessage(ChatColor.GREEN + "Preparing Timings Report..."); + lastReport = now; + Map parent = createObject( + // Get some basic system details about the server + pair("version", Bukkit.getVersion()), + pair("maxplayers", Bukkit.getMaxPlayers()), + pair("start", TimingsManager.timingStart / 1000), + pair("end", System.currentTimeMillis() / 1000), + pair("sampletime", (System.currentTimeMillis() - TimingsManager.timingStart) / 1000) + ); + if (!TimingsManager.privacy) { + appendObjectData(parent, + pair("server", Bukkit.getUnsafe().getTimingsServerName()), + pair("motd", Bukkit.getServer().getMotd()), + pair("online-mode", Bukkit.getServer().getOnlineMode()), + pair("icon", Bukkit.getServer().getServerIcon().getData()) + ); + } + + final Runtime runtime = Runtime.getRuntime(); + RuntimeMXBean runtimeBean = ManagementFactory.getRuntimeMXBean(); + + parent.put("system", createObject( + pair("timingcost", getCost()), + pair("name", System.getProperty("os.name")), + pair("version", System.getProperty("os.version")), + pair("jvmversion", System.getProperty("java.version")), + pair("arch", System.getProperty("os.arch")), + pair("maxmem", runtime.maxMemory()), + pair("cpu", runtime.availableProcessors()), + pair("runtime", ManagementFactory.getRuntimeMXBean().getUptime()), + pair("flags", StringUtils.join(runtimeBean.getInputArguments(), " ")), + pair("gc", toObjectMapper(ManagementFactory.getGarbageCollectorMXBeans(), input -> pair(input.getName(), toArray(input.getCollectionCount(), input.getCollectionTime())))) + ) + ); + + Set tileEntityTypeSet = Sets.newHashSet(); + Set entityTypeSet = Sets.newHashSet(); + + int size = HISTORY.size(); + TimingHistory[] history = new TimingHistory[size + 1]; + int i = 0; + for (TimingHistory timingHistory : HISTORY) { + tileEntityTypeSet.addAll(timingHistory.tileEntityTypeSet); + entityTypeSet.addAll(timingHistory.entityTypeSet); + history[i++] = timingHistory; + } + + history[i] = new TimingHistory(); // Current snapshot + tileEntityTypeSet.addAll(history[i].tileEntityTypeSet); + entityTypeSet.addAll(history[i].entityTypeSet); + + + Map handlers = createObject(); + Map groupData; + synchronized (TimingIdentifier.GROUP_MAP) { + for (TimingIdentifier.TimingGroup group : TimingIdentifier.GROUP_MAP.values()) { + synchronized (group.handlers) { + for (TimingHandler id : group.handlers) { + + if (!id.isTimed() && !id.isSpecial()) { + continue; + } + + String name = id.identifier.name; + if (name.startsWith("##")) { + name = name.substring(3); + } + handlers.put(id.id, toArray( + group.id, + name + )); + } + } + } + + groupData = toObjectMapper( + TimingIdentifier.GROUP_MAP.values(), group -> pair(group.id, group.name)); + } + + parent.put("idmap", createObject( + pair("groups", groupData), + pair("handlers", handlers), + pair("worlds", toObjectMapper(TimingHistory.worldMap.entrySet(), input -> pair(input.getValue(), input.getKey()))), + pair("tileentity", + toObjectMapper(tileEntityTypeSet, input -> pair(input.ordinal(), input.name()))), + pair("entity", + toObjectMapper(entityTypeSet, input -> pair(input.getTypeId(), input.name()))) + )); + + // Information about loaded plugins + + parent.put("plugins", toObjectMapper(Bukkit.getPluginManager().getPlugins(), + plugin -> pair(plugin.getName(), createObject( + pair("version", plugin.getDescription().getVersion()), + pair("description", String.valueOf(plugin.getDescription().getDescription()).trim()), + pair("website", plugin.getDescription().getWebsite()), + pair("authors", StringUtils.join(plugin.getDescription().getAuthors(), ", ")) + )))); + + + + // Information on the users Config + + parent.put("config", createObject( + pair("spigot", mapAsJSON(Bukkit.spigot().getSpigotConfig(), null)), + pair("bukkit", mapAsJSON(Bukkit.spigot().getBukkitConfig(), null)), + pair("paper", mapAsJSON(Bukkit.spigot().getPaperConfig(), null)) + )); + + new TimingsExport(listeners, parent, history).start(); + } + + static long getCost() { + // Benchmark the users System.nanotime() for cost basis + int passes = 100; + TimingHandler SAMPLER1 = Timings.ofSafe("Timings Sampler 1"); + TimingHandler SAMPLER2 = Timings.ofSafe("Timings Sampler 2"); + TimingHandler SAMPLER3 = Timings.ofSafe("Timings Sampler 3"); + TimingHandler SAMPLER4 = Timings.ofSafe("Timings Sampler 4"); + TimingHandler SAMPLER5 = Timings.ofSafe("Timings Sampler 5"); + TimingHandler SAMPLER6 = Timings.ofSafe("Timings Sampler 6"); + + long start = System.nanoTime(); + for (int i = 0; i < passes; i++) { + SAMPLER1.startTiming(); + SAMPLER2.startTiming(); + SAMPLER3.startTiming(); + SAMPLER3.stopTiming(); + SAMPLER4.startTiming(); + SAMPLER5.startTiming(); + SAMPLER6.startTiming(); + SAMPLER6.stopTiming(); + SAMPLER5.stopTiming(); + SAMPLER4.stopTiming(); + SAMPLER2.stopTiming(); + SAMPLER1.stopTiming(); + } + long timingsCost = (System.nanoTime() - start) / passes / 6; + SAMPLER1.reset(true); + SAMPLER2.reset(true); + SAMPLER3.reset(true); + SAMPLER4.reset(true); + SAMPLER5.reset(true); + SAMPLER6.reset(true); + return timingsCost; + } + + private static JSONObject mapAsJSON(ConfigurationSection config, String parentKey) { + + JSONObject object = new JSONObject(); + for (String key : config.getKeys(false)) { + String fullKey = (parentKey != null ? parentKey + "." + key : key); + if (fullKey.equals("database") || fullKey.equals("settings.bungeecord-addresses") || TimingsManager.hiddenConfigs.contains(fullKey)) { + continue; + } + final Object val = config.get(key); + + object.put(key, valAsJSON(val, fullKey)); + } + return object; + } + + private static Object valAsJSON(Object val, final String parentKey) { + if (!(val instanceof MemorySection)) { + if (val instanceof List) { + Iterable v = (Iterable) val; + return toArrayMapper(v, input -> valAsJSON(input, parentKey)); + } else { + return val.toString(); + } + } else { + return mapAsJSON((ConfigurationSection) val, parentKey); + } + } + + @Override + public void run() { + out.put("data", toArrayMapper(history, TimingHistory::export)); + + + String response = null; + String timingsURL = null; + try { + HttpURLConnection con = (HttpURLConnection) new URL("http://timings.aikar.co/post").openConnection(); + con.setDoOutput(true); + String hostName = "BrokenHost"; + try { + hostName = InetAddress.getLocalHost().getHostName(); + } catch (Exception ignored) {} + con.setRequestProperty("User-Agent", "Paper/" + Bukkit.getUnsafe().getTimingsServerName() + "/" + hostName); + con.setRequestMethod("POST"); + con.setInstanceFollowRedirects(false); + + OutputStream request = new GZIPOutputStream(con.getOutputStream()) {{ + this.def.setLevel(7); + }}; + + request.write(JSONValue.toJSONString(out).getBytes("UTF-8")); + request.close(); + + response = getResponse(con); + + if (con.getResponseCode() != 302) { + listeners.sendMessage( + ChatColor.RED + "Upload Error: " + con.getResponseCode() + ": " + con.getResponseMessage()); + listeners.sendMessage(ChatColor.RED + "Check your logs for more information"); + if (response != null) { + Bukkit.getLogger().log(Level.SEVERE, response); + } + return; + } + + timingsURL = con.getHeaderField("Location"); + listeners.sendMessage(ChatColor.GREEN + "View Timings Report: " + timingsURL); + + if (response != null && !response.isEmpty()) { + Bukkit.getLogger().log(Level.INFO, "Timing Response: " + response); + } + } catch (IOException ex) { + listeners.sendMessage(ChatColor.RED + "Error uploading timings, check your logs for more information"); + if (response != null) { + Bukkit.getLogger().log(Level.SEVERE, response); + } + Bukkit.getLogger().log(Level.SEVERE, "Could not paste timings", ex); + } finally { + this.listeners.done(timingsURL); + } + } + + private String getResponse(HttpURLConnection con) throws IOException { + InputStream is = null; + try { + is = con.getInputStream(); + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + + byte[] b = new byte[1024]; + int bytesRead; + while ((bytesRead = is.read(b)) != -1) { + bos.write(b, 0, bytesRead); + } + return bos.toString(); + + } catch (IOException ex) { + listeners.sendMessage(ChatColor.RED + "Error uploading timings, check your logs for more information"); + Bukkit.getLogger().log(Level.WARNING, con.getResponseMessage(), ex); + return null; + } finally { + if (is != null) { + is.close(); + } + } + } +} diff --git a/src/main/java/co/aikar/timings/TimingsManager.java b/src/main/java/co/aikar/timings/TimingsManager.java new file mode 100644 index 000000000..ef824d701 --- /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.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.logging.Level; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public final class TimingsManager { + static final Map TIMING_MAP = LoadingMap.of( + new ConcurrentHashMap<>(4096, .5F), TimingHandler::new + ); + public static final FullServerTickHandler FULL_SERVER_TICK = new FullServerTickHandler(); + public static final TimingHandler TIMINGS_TICK = Timings.ofSafe("Timings Tick", FULL_SERVER_TICK); + public static final Timing PLUGIN_GROUP_HANDLER = Timings.ofSafe("Plugins"); + public static List 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 000000000..bf3e059fe --- /dev/null +++ b/src/main/java/co/aikar/timings/TimingsReportListener.java @@ -0,0 +1,75 @@ +package co.aikar.timings; + +import com.google.common.collect.Lists; +import org.apache.commons.lang.Validate; +import org.bukkit.Bukkit; +import org.bukkit.command.CommandSender; +import org.bukkit.command.ConsoleCommandSender; +import org.bukkit.command.MessageCommandSender; +import org.bukkit.command.RemoteConsoleCommandSender; + +import java.util.List; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +@SuppressWarnings("WeakerAccess") +public class TimingsReportListener implements MessageCommandSender { + private final List 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) { + Validate.notNull(senders); + Validate.notEmpty(senders); + + this.senders = Lists.newArrayList(senders); + this.onDone = onDone; + } + + @Nullable + public String getTimingsURL() { + return timingsURL; + } + + public void done() { + done(null); + } + + public void done(@Nullable String url) { + this.timingsURL = url; + if (onDone != null) { + onDone.run(); + } + for (CommandSender sender : senders) { + if (sender instanceof TimingsReportListener) { + ((TimingsReportListener) sender).done(); + } + } + } + + @Override + public void sendMessage(@NotNull String message) { + senders.forEach((sender) -> sender.sendMessage(message)); + } + + public void addConsoleIfNeeded() { + boolean hasConsole = false; + for (CommandSender sender : this.senders) { + if (sender instanceof ConsoleCommandSender || sender instanceof RemoteConsoleCommandSender) { + hasConsole = true; + } + } + if (!hasConsole) { + this.senders.add(Bukkit.getConsoleSender()); + } + } +} diff --git a/src/main/java/co/aikar/timings/UnsafeTimingHandler.java b/src/main/java/co/aikar/timings/UnsafeTimingHandler.java new file mode 100644 index 000000000..632c49615 --- /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 000000000..80155072d --- /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 decrement(key, -amount); + } + public long increment(@Nullable T key, long amount) { + Long count = this.getCount(key); + count += amount; + this.counts.put(key, count); + return count; + } + + public long getCount(@Nullable T key) { + return this.counts.getOrDefault(key, 0L); + } + + @NotNull + @Override + protected Map delegate() { + return this.counts; + } +} diff --git a/src/main/java/co/aikar/util/JSONUtil.java b/src/main/java/co/aikar/util/JSONUtil.java new file mode 100644 index 000000000..190bf0598 --- /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 000000000..63a899c7d --- /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 000000000..aedbb0332 --- /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 000000000..5989ee212 --- /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 0822b8e53..940c643df 100644 --- a/src/main/java/org/bukkit/Bukkit.java +++ b/src/main/java/org/bukkit/Bukkit.java @@ -574,7 +574,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 11c5c2054..c197e3811 100644 --- a/src/main/java/org/bukkit/Server.java +++ b/src/main/java/org/bukkit/Server.java @@ -1250,6 +1250,26 @@ public interface Server extends PluginMessageRecipient { throw new UnsupportedOperationException( "Not supported yet." ); } + // Paper start + @NotNull + public org.bukkit.configuration.file.YamlConfiguration getBukkitConfig() + { + throw new UnsupportedOperationException( "Not supported yet." ); + } + + @NotNull + public org.bukkit.configuration.file.YamlConfiguration getSpigotConfig() + { + throw new UnsupportedOperationException("Not supported yet."); + } + + @NotNull + public org.bukkit.configuration.file.YamlConfiguration getPaperConfig() + { + throw new UnsupportedOperationException("Not supported yet."); + } + // Paper end + /** * Sends the component to the player * diff --git a/src/main/java/org/bukkit/UnsafeValues.java b/src/main/java/org/bukkit/UnsafeValues.java index 247d194f8..72c5501e8 100644 --- a/src/main/java/org/bukkit/UnsafeValues.java +++ b/src/main/java/org/bukkit/UnsafeValues.java @@ -69,4 +69,12 @@ public interface UnsafeValues { * @return true if a file matching this key was found and deleted */ boolean removeAdvancement(NamespacedKey key); + + // Paper start + /** + * Server name to report to timings v2 + * @return name + */ + String getTimingsServerName(); + // Paper end } diff --git a/src/main/java/org/bukkit/command/BufferedCommandSender.java b/src/main/java/org/bukkit/command/BufferedCommandSender.java new file mode 100644 index 000000000..f9a00aecc --- /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 4bfc21468..03bdc1622 100644 --- a/src/main/java/org/bukkit/command/Command.java +++ b/src/main/java/org/bukkit/command/Command.java @@ -33,7 +33,8 @@ public abstract class Command { protected String usageMessage; private String permission; private String permissionMessage; - public org.spigotmc.CustomTimingsHandler timings; // Spigot + public co.aikar.timings.Timing timings; // Paper + @NotNull public String getTimingName() {return getName();} // Paper protected Command(@NotNull String name) { this(name, "", "/" + name, new ArrayList()); @@ -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 d6c8938b1..a6ad94ef9 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 000000000..ca1893e9f --- /dev/null +++ b/src/main/java/org/bukkit/command/MessageCommandSender.java @@ -0,0 +1,114 @@ +package org.bukkit.command; + +import org.apache.commons.lang.NotImplementedException; +import org.bukkit.Bukkit; +import org.bukkit.Server; +import org.bukkit.permissions.Permission; +import org.bukkit.permissions.PermissionAttachment; +import org.bukkit.permissions.PermissionAttachmentInfo; +import org.bukkit.plugin.Plugin; + +import java.util.Set; +import org.jetbrains.annotations.NotNull; + +/** + * For when all you care about is just messaging + */ +public interface MessageCommandSender extends CommandSender { + + @Override + default void sendMessage(@NotNull String[] messages) { + for (String message : messages) { + sendMessage(message); + } + } + + @NotNull + @Override + default Server getServer() { + return Bukkit.getServer(); + } + + @NotNull + @Override + default String getName() { + throw new NotImplementedException(); + } + + @Override + default boolean isOp() { + throw new NotImplementedException(); + } + + @Override + default void setOp(boolean value) { + throw new NotImplementedException(); + } + + @Override + default boolean isPermissionSet(@NotNull String name) { + throw new NotImplementedException(); + } + + @Override + default boolean isPermissionSet(@NotNull Permission perm) { + throw new NotImplementedException(); + } + + @Override + default boolean hasPermission(@NotNull String name) { + throw new NotImplementedException(); + } + + @Override + default boolean hasPermission(@NotNull Permission perm) { + throw new NotImplementedException(); + } + + @NotNull + @Override + default PermissionAttachment addAttachment(@NotNull Plugin plugin, @NotNull String name, boolean value) { + throw new NotImplementedException(); + } + + @NotNull + @Override + default PermissionAttachment addAttachment(@NotNull Plugin plugin) { + throw new NotImplementedException(); + } + + @NotNull + @Override + default PermissionAttachment addAttachment(@NotNull Plugin plugin, @NotNull String name, boolean value, int ticks) { + throw new NotImplementedException(); + } + + @NotNull + @Override + default PermissionAttachment addAttachment(@NotNull Plugin plugin, int ticks) { + throw new NotImplementedException(); + } + + @Override + default void removeAttachment(@NotNull PermissionAttachment attachment) { + throw new NotImplementedException(); + } + + @Override + default void recalculatePermissions() { + throw new NotImplementedException(); + } + + @NotNull + @Override + default Set getEffectivePermissions() { + throw new NotImplementedException(); + } + + @NotNull + @Override + default Spigot spigot() { + throw new NotImplementedException(); + } + +} diff --git a/src/main/java/org/bukkit/command/SimpleCommandMap.java b/src/main/java/org/bukkit/command/SimpleCommandMap.java index 81e4fa573..f020cb04e 100644 --- a/src/main/java/org/bukkit/command/SimpleCommandMap.java +++ b/src/main/java/org/bukkit/command/SimpleCommandMap.java @@ -15,7 +15,6 @@ import org.bukkit.command.defaults.BukkitCommand; import org.bukkit.command.defaults.HelpCommand; import org.bukkit.command.defaults.PluginsCommand; import org.bukkit.command.defaults.ReloadCommand; -import org.bukkit.command.defaults.TimingsCommand; import org.bukkit.command.defaults.VersionCommand; import org.bukkit.entity.Player; import org.bukkit.util.StringUtil; @@ -35,7 +34,7 @@ public class SimpleCommandMap implements CommandMap { register("bukkit", new VersionCommand("version")); register("bukkit", new ReloadCommand("reload")); register("bukkit", new PluginsCommand("plugins")); - register("bukkit", new TimingsCommand("timings")); + register("bukkit", new co.aikar.timings.TimingsCommand("timings")); // Paper } public void setFallbackCommands() { @@ -67,6 +66,7 @@ public class SimpleCommandMap implements CommandMap { */ @Override public boolean register(@NotNull String label, @NotNull String fallbackPrefix, @NotNull Command command) { + command.timings = co.aikar.timings.TimingsManager.getCommandTiming(fallbackPrefix, command); // Paper label = label.toLowerCase(java.util.Locale.ENGLISH).trim(); fallbackPrefix = fallbackPrefix.toLowerCase(java.util.Locale.ENGLISH).trim(); boolean registered = register(label, command, false, fallbackPrefix); @@ -143,16 +143,22 @@ public class SimpleCommandMap implements CommandMap { return false; } + // Paper start - Plugins do weird things to workaround normal registration + if (target.timings == null) { + target.timings = co.aikar.timings.TimingsManager.getCommandTiming(null, target); + } + // Paper end + try { - target.timings.startTiming(); // Spigot + try (co.aikar.timings.Timing ignored = target.timings.startTiming()) { // Paper - use try with resources // Note: we don't return the result of target.execute as thats success / failure, we return handled (true) or not handled (false) target.execute(sender, sentCommandLabel, Arrays.copyOfRange(args, 1, args.length)); - target.timings.stopTiming(); // Spigot + } // target.timings.stopTiming(); // Spigot // Paper } catch (CommandException ex) { - target.timings.stopTiming(); // Spigot + //target.timings.stopTiming(); // Spigot // Paper throw ex; } catch (Throwable ex) { - target.timings.stopTiming(); // Spigot + //target.timings.stopTiming(); // Spigot // Paper throw new CommandException("Unhandled exception executing '" + commandLine + "' in " + target, ex); } diff --git a/src/main/java/org/bukkit/command/defaults/TimingsCommand.java b/src/main/java/org/bukkit/command/defaults/TimingsCommand.java deleted file mode 100644 index 6023e4f61..000000000 --- a/src/main/java/org/bukkit/command/defaults/TimingsCommand.java +++ /dev/null @@ -1,253 +0,0 @@ -package org.bukkit.command.defaults; - -import com.google.common.collect.ImmutableList; -import java.io.File; -import java.io.IOException; -import java.io.PrintStream; -import java.util.ArrayList; -import java.util.List; -import org.apache.commons.lang.Validate; -import org.bukkit.Bukkit; -import org.bukkit.ChatColor; -import org.bukkit.command.CommandSender; -import org.bukkit.event.Event; -import org.bukkit.event.HandlerList; -import org.bukkit.plugin.Plugin; -import org.bukkit.plugin.RegisteredListener; -import org.bukkit.plugin.TimedRegisteredListener; -import org.bukkit.util.StringUtil; -import org.jetbrains.annotations.NotNull; - -// Spigot start -// CHECKSTYLE:OFF -import java.io.ByteArrayOutputStream; -import java.io.OutputStream; -import java.net.HttpURLConnection; -import java.net.URL; -import java.util.logging.Level; -import org.bukkit.command.RemoteConsoleCommandSender; -import org.bukkit.plugin.SimplePluginManager; -import org.spigotmc.CustomTimingsHandler; -// CHECKSTYLE:ON -// Spigot end - -public class TimingsCommand extends BukkitCommand { - private static final List 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; - } - if (true) { executeSpigotTimings(sender, args); return true; } // Spigot - if (!sender.getServer().getPluginManager().useTimings()) { - sender.sendMessage("Please enable timings by setting \"settings.plugin-profiling\" to true in bukkit.yml"); - return true; - } - - boolean separate = "separate".equalsIgnoreCase(args[0]); - if ("reset".equalsIgnoreCase(args[0])) { - for (HandlerList handlerList : HandlerList.getHandlerLists()) { - for (RegisteredListener listener : handlerList.getRegisteredListeners()) { - if (listener instanceof TimedRegisteredListener) { - ((TimedRegisteredListener) listener).reset(); - } - } - } - sender.sendMessage("Timings reset"); - } else if ("merged".equalsIgnoreCase(args[0]) || separate) { - - int index = 0; - int pluginIdx = 0; - File timingFolder = new File("timings"); - timingFolder.mkdirs(); - File timings = new File(timingFolder, "timings.txt"); - File names = null; - while (timings.exists()) timings = new File(timingFolder, "timings" + (++index) + ".txt"); - PrintStream fileTimings = null; - PrintStream fileNames = null; - try { - fileTimings = new PrintStream(timings); - if (separate) { - names = new File(timingFolder, "names" + index + ".txt"); - fileNames = new PrintStream(names); - } - for (Plugin plugin : Bukkit.getPluginManager().getPlugins()) { - pluginIdx++; - long totalTime = 0; - if (separate) { - fileNames.println(pluginIdx + " " + plugin.getDescription().getFullName()); - fileTimings.println("Plugin " + pluginIdx); - } - else fileTimings.println(plugin.getDescription().getFullName()); - for (RegisteredListener listener : HandlerList.getRegisteredListeners(plugin)) { - if (listener instanceof TimedRegisteredListener) { - TimedRegisteredListener trl = (TimedRegisteredListener) listener; - long time = trl.getTotalTime(); - int count = trl.getCount(); - if (count == 0) continue; - long avg = time / count; - totalTime += time; - Class 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) { - Validate.notNull(sender, "Sender cannot be null"); - Validate.notNull(args, "Arguments cannot be null"); - Validate.notNull(alias, "Alias cannot be null"); - - if (args.length == 1) { - return StringUtil.copyPartialMatches(args[0], TIMINGS_SUBCOMMANDS, new ArrayList(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 fcf473396..8c3830cca 100644 --- a/src/main/java/org/bukkit/entity/Player.java +++ b/src/main/java/org/bukkit/entity/Player.java @@ -1572,6 +1572,11 @@ public interface Player extends HumanEntity, Conversable, OfflinePlayer, PluginM public void sendMessage(@NotNull net.md_5.bungee.api.ChatMessageType position, @NotNull net.md_5.bungee.api.chat.BaseComponent... components) { throw new UnsupportedOperationException("Not supported yet."); } + + public int getPing() + { + throw new UnsupportedOperationException( "Not supported yet." ); + } } @NotNull diff --git a/src/main/java/org/bukkit/plugin/SimplePluginManager.java b/src/main/java/org/bukkit/plugin/SimplePluginManager.java index f648c5989..78a2d2f8d 100644 --- a/src/main/java/org/bukkit/plugin/SimplePluginManager.java +++ b/src/main/java/org/bukkit/plugin/SimplePluginManager.java @@ -297,7 +297,6 @@ public final class SimplePluginManager implements PluginManager { } } - org.bukkit.command.defaults.TimingsCommand.timingStart = System.nanoTime(); // Spigot return result.toArray(new Plugin[result.size()]); } @@ -336,7 +335,7 @@ public final class SimplePluginManager implements PluginManager { if (result != null) { plugins.add(result); - lookupNames.put(result.getDescription().getName(), result); + lookupNames.put(result.getDescription().getName().toLowerCase(java.util.Locale.ENGLISH), result); // Paper } return result; @@ -364,7 +363,7 @@ public final class SimplePluginManager implements PluginManager { @Override @Nullable public synchronized Plugin getPlugin(@NotNull String name) { - return lookupNames.get(name.replace(' ', '_')); + return lookupNames.get(name.replace(' ', '_').toLowerCase(java.util.Locale.ENGLISH)); // Paper } @Override @@ -577,7 +576,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)); @@ -774,7 +774,7 @@ public final class SimplePluginManager implements PluginManager { @Override public boolean useTimings() { - return useTimings; + return co.aikar.timings.Timings.isTimingsEnabled(); // Spigot } /** @@ -783,6 +783,6 @@ public final class SimplePluginManager implements PluginManager { * @param use True if per event timing code should be used */ public void useTimings(boolean use) { - useTimings = use; + co.aikar.timings.Timings.setTimingsEnabled(use); // Paper } } diff --git a/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java b/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java index 1173e433a..82e379d16 100644 --- a/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java +++ b/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java @@ -53,7 +53,6 @@ public final class JavaPluginLoader implements PluginLoader { private final Pattern[] fileFilters = new Pattern[] { Pattern.compile("\\.jar$"), }; private final Map> classes = new ConcurrentHashMap>(); private final List loaders = new CopyOnWriteArrayList(); - public static final CustomTimingsHandler pluginParentTimer = new CustomTimingsHandler("** Plugins"); // Spigot /** * This class was not meant to be constructed explicitly @@ -302,27 +301,21 @@ public final class JavaPluginLoader implements PluginLoader { } } - final CustomTimingsHandler timings = new CustomTimingsHandler("Plugin: " + plugin.getDescription().getFullName() + " Event: " + listener.getClass().getName() + "::" + method.getName()+"("+eventClass.getSimpleName()+")", pluginParentTimer); // Spigot - EventExecutor executor = new EventExecutor() { + EventExecutor executor = new co.aikar.timings.TimedEventExecutor(new EventExecutor() { // Paper @Override - public void execute(@NotNull Listener listener, @NotNull Event event) throws EventException { + public void execute(@NotNull Listener listener, @NotNull Event event) throws EventException { // Paper try { if (!eventClass.isAssignableFrom(event.getClass())) { return; } - // Spigot start - boolean isAsync = event.isAsynchronous(); - if (!isAsync) timings.startTiming(); method.invoke(listener, event); - if (!isAsync) timings.stopTiming(); - // Spigot end } catch (InvocationTargetException ex) { throw new EventException(ex.getCause()); } catch (Throwable t) { throw new EventException(t); } } - }; + }, plugin, method, eventClass); // Paper if (false) { // Spigot - RL handles useTimings check now eventSet.add(new TimedRegisteredListener(listener, executor, eh.priority(), plugin, eh.ignoreCancelled())); } else { diff --git a/src/main/java/org/bukkit/plugin/java/PluginClassLoader.java b/src/main/java/org/bukkit/plugin/java/PluginClassLoader.java index 0ffc1dfdb..b859796b4 100644 --- a/src/main/java/org/bukkit/plugin/java/PluginClassLoader.java +++ b/src/main/java/org/bukkit/plugin/java/PluginClassLoader.java @@ -24,7 +24,8 @@ import org.jetbrains.annotations.Nullable; /** * A ClassLoader for plugins, to allow shared classes across multiple plugins */ -final class PluginClassLoader extends URLClassLoader { +public final class PluginClassLoader extends URLClassLoader { // Spigot + public JavaPlugin getPlugin() { return plugin; } // Spigot private final JavaPluginLoader loader; private final Map> classes = new ConcurrentHashMap>(); private final PluginDescriptionFile description; diff --git a/src/main/java/org/bukkit/util/CachedServerIcon.java b/src/main/java/org/bukkit/util/CachedServerIcon.java index 5ca863b36..612958a33 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 b71235f69..3cbe5c2bb 100644 --- a/src/main/java/org/spigotmc/CustomTimingsHandler.java +++ b/src/main/java/org/spigotmc/CustomTimingsHandler.java @@ -1,3 +1,26 @@ +/* + * This file is licensed under the MIT License (MIT). + * + * Copyright (c) 2014 Daniel Ennis + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ package org.spigotmc; import java.io.PrintStream; @@ -5,155 +28,84 @@ import java.util.Queue; import java.util.concurrent.ConcurrentLinkedQueue; import org.bukkit.Bukkit; import org.bukkit.World; -import org.bukkit.command.defaults.TimingsCommand; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.bukkit.plugin.AuthorNagException; +import org.bukkit.plugin.Plugin; +import co.aikar.timings.Timing; +import co.aikar.timings.Timings; +import co.aikar.timings.TimingsManager; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.logging.Level; /** - * Provides custom timing sections for /timings merged. + * This is here for legacy purposes incase any plugin used it. + * + * If you use this, migrate ASAP as this will be removed in the future! + * + * @deprecated + * @see co.aikar.timings.Timings#of */ -public class CustomTimingsHandler -{ +@Deprecated +public final class CustomTimingsHandler { + private final Timing handler; + private static Boolean sunReflectAvailable; + private static Method getCallerClass; - 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; + public CustomTimingsHandler(@NotNull String name) { + if (sunReflectAvailable == null) { + String javaVer = System.getProperty("java.version"); + String[] elements = javaVer.split("\\."); - public CustomTimingsHandler(@NotNull String name) - { - this( name, null ); - } - - public CustomTimingsHandler(@NotNull String name, @Nullable CustomTimingsHandler parent) - { - this.name = name; - this.parent = parent; - HANDLERS.add( this ); - } + int major = Integer.parseInt(elements.length >= 2 ? elements[1] : javaVer); + if (major <= 8) { + sunReflectAvailable = true; - /** - * Prints the timings and extra data to the given stream. - * - * @param printStream output stream - */ - public static void printTimings(@NotNull PrintStream printStream) - { - printStream.println( "Minecraft" ); - for ( CustomTimingsHandler timings : HANDLERS ) - { - long time = timings.totalTime; - long count = timings.count; - if ( count == 0 ) - { - continue; + try { + Class reflection = Class.forName("sun.reflect.Reflection"); + getCallerClass = reflection.getMethod("getCallerClass", int.class); + } catch (ClassNotFoundException | NoSuchMethodException ignored) { + } + } else { + sunReflectAvailable = false; } - long avg = time / count; - - printStream.println( " " + timings.name + " Time: " + time + " Count: " + count + " Avg: " + avg + " Violations: " + timings.violations ); - } - printStream.println( "# Version " + Bukkit.getVersion() ); - int entities = 0; - int livingEntities = 0; - for ( World world : Bukkit.getWorlds() ) - { - entities += world.getEntities().size(); - livingEntities += world.getLivingEntities().size(); } - printStream.println( "# Entities " + entities ); - printStream.println( "# LivingEntities " + livingEntities ); - } - /** - * Resets all timings. - */ - public static void reload() - { - if ( Bukkit.getPluginManager().useTimings() ) - { - for ( CustomTimingsHandler timings : HANDLERS ) - { - timings.reset(); + Class calling = null; + if (sunReflectAvailable) { + try { + calling = (Class) getCallerClass.invoke(null, 2); + } catch (IllegalAccessException | InvocationTargetException ignored) { } } - TimingsCommand.timingStart = System.nanoTime(); - } - /** - * Ticked every tick by CraftBukkit to count the number of times a timer - * caused TPS loss. - */ - public static void tick() - { - if ( Bukkit.getPluginManager().useTimings() ) - { - for ( CustomTimingsHandler timings : HANDLERS ) - { - if ( timings.curTickTotal > 50000000 ) - { - timings.violations += Math.ceil( timings.curTickTotal / 50000000 ); - } - timings.curTickTotal = 0; - timings.timingDepth = 0; // incase reset messes this up - } - } - } + Timing timing; - /** - * Starts timing to track a section of code. - */ - public void startTiming() - { - // If second condtion fails we are already timing - if ( Bukkit.getPluginManager().useTimings() && ++timingDepth == 1 ) - { - start = System.nanoTime(); - if ( parent != null && ++parent.timingDepth == 1 ) - { - parent.start = start; - } - } - } + Plugin plugin = null; + try { + plugin = TimingsManager.getPluginByClassloader(calling); + } catch (Exception ignored) {} - /** - * Stops timing a section of code. - */ - public void stopTiming() - { - if ( Bukkit.getPluginManager().useTimings() ) - { - if ( --timingDepth != 0 || start == 0 ) - { - return; - } - long diff = System.nanoTime() - start; - totalTime += diff; - curTickTotal += diff; - count++; - start = 0; - if ( parent != null ) - { - parent.stopTiming(); + new AuthorNagException("Deprecated use of CustomTimingsHandler. Please Switch to Timings.of ASAP").printStackTrace(); + if (plugin != null) { + timing = Timings.of(plugin, "(Deprecated API) " + name); + } else { + try { + final Method ofSafe = TimingsManager.class.getDeclaredMethod("getHandler", String.class, String.class, Timing.class); + ofSafe.setAccessible(true); + timing = (Timing) ofSafe.invoke(null,"Minecraft", "(Deprecated API) " + name, null); + } catch (Exception e) { + e.printStackTrace(); + Bukkit.getLogger().log(Level.SEVERE, "This handler could not be registered"); + timing = Timings.NULL_HANDLER; } } + handler = timing; } - /** - * Reset this timer, setting all values to zero. - */ - public void reset() - { - count = 0; - violations = 0; - curTickTotal = 0; - totalTime = 0; - start = 0; - timingDepth = 0; - } + public void startTiming() { handler.startTiming(); } + public void stopTiming() { handler.stopTiming(); } + } -- 2.21.0