From 814a2afee3c6dd679eda99f04850f658b9a947e3 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 0000000..e5a98af --- /dev/null +++ b/src/main/java/co/aikar/timings/FullServerTickHandler.java @@ -0,0 +1,81 @@ +package co.aikar.timings; + +import static co.aikar.timings.TimingsManager.*; + +public class FullServerTickHandler extends TimingHandler { + private static final TimingIdentifier IDENTITY = new TimingIdentifier("Minecraft", "Full Server Tick", null, false); + final TimingData minuteData; + double avgFreeMemory = -1D; + double avgUsedMemory = -1D; + FullServerTickHandler() { + super(IDENTITY); + minuteData = new TimingData(id); + + TIMING_MAP.put(IDENTITY, this); + } + + @Override + public Timing startTiming() { + if (TimingsManager.needsFullReset) { + TimingsManager.resetTimings(); + } else if (TimingsManager.needsRecheckEnabled) { + TimingsManager.recheckEnabled(); + } + return super.startTiming(); + } + + @Override + public void stopTiming() { + super.stopTiming(); + if (!isEnabled()) { + return; + } + if (TimingHistory.timedTicks % 20 == 0) { + final Runtime runtime = Runtime.getRuntime(); + double usedMemory = runtime.totalMemory() - runtime.freeMemory(); + double freeMemory = runtime.maxMemory() - usedMemory; + if (this.avgFreeMemory == -1) { + this.avgFreeMemory = freeMemory; + } else { + this.avgFreeMemory = (this.avgFreeMemory * (59 / 60D)) + (freeMemory * (1 / 60D)); + } + + if (this.avgUsedMemory == -1) { + this.avgUsedMemory = usedMemory; + } else { + this.avgUsedMemory = (this.avgUsedMemory * (59 / 60D)) + (usedMemory * (1 / 60D)); + } + } + + long start = System.nanoTime(); + TimingsManager.tick(); + long diff = System.nanoTime() - start; + CURRENT = TIMINGS_TICK; + TIMINGS_TICK.addDiff(diff); + // addDiff for TIMINGS_TICK incremented this, bring it back down to 1 per tick. + record.setCurTickCount(record.getCurTickCount()-1); + + minuteData.setCurTickTotal(record.getCurTickTotal()); + minuteData.setCurTickCount(1); + + boolean violated = isViolated(); + minuteData.processTick(violated); + TIMINGS_TICK.processTick(violated); + processTick(violated); + + + if (TimingHistory.timedTicks % 1200 == 0) { + MINUTE_REPORTS.add(new TimingHistory.MinuteReport()); + TimingHistory.resetTicks(false); + minuteData.reset(); + } + if (TimingHistory.timedTicks % Timings.getHistoryInterval() == 0) { + TimingsManager.HISTORY.add(new TimingHistory()); + TimingsManager.resetTimings(); + } + } + + 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 0000000..8c43e20 --- /dev/null +++ b/src/main/java/co/aikar/timings/NullTimingHandler.java @@ -0,0 +1,61 @@ +/* + * This file is licensed under the MIT License (MIT). + * + * Copyright (c) 2014 Daniel Ennis + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package co.aikar.timings; + +public final class NullTimingHandler implements Timing { + @Override + public Timing startTiming() { + return this; + } + + @Override + public void stopTiming() { + + } + + @Override + public Timing startTimingIfSync() { + return this; + } + + @Override + public void stopTimingIfSync() { + + } + + @Override + public void abort() { + + } + + @Override + public TimingHandler getTimingHandler() { + return null; + } + + @Override + public void close() { + + } +} diff --git a/src/main/java/co/aikar/timings/TimedEventExecutor.java b/src/main/java/co/aikar/timings/TimedEventExecutor.java new file mode 100644 index 0000000..96057fc --- /dev/null +++ b/src/main/java/co/aikar/timings/TimedEventExecutor.java @@ -0,0 +1,81 @@ +/* + * This file is licensed under the MIT License (MIT). + * + * Copyright (c) 2014 Daniel Ennis + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package co.aikar.timings; + +import org.bukkit.Bukkit; +import org.bukkit.event.Event; +import org.bukkit.event.EventException; +import org.bukkit.event.Listener; +import org.bukkit.plugin.EventExecutor; +import org.bukkit.plugin.Plugin; + +import java.lang.reflect.Method; + +public class TimedEventExecutor implements EventExecutor { + + private final EventExecutor executor; + private final Timing timings; + + /** + * Wraps an event executor and associates a timing handler to it. + * + * @param executor + * @param plugin + * @param method + * @param eventClass + */ + public TimedEventExecutor(EventExecutor executor, Plugin plugin, Method method, 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(Listener listener, Event event) throws EventException { + if (event.isAsynchronous() || !Timings.timingsEnabled || !Bukkit.isPrimaryThread()) { + executor.execute(listener, event); + return; + } + timings.startTiming(); + executor.execute(listener, event); + timings.stopTiming(); + } +} diff --git a/src/main/java/co/aikar/timings/Timing.java b/src/main/java/co/aikar/timings/Timing.java new file mode 100644 index 0000000..8b2d1b8 --- /dev/null +++ b/src/main/java/co/aikar/timings/Timing.java @@ -0,0 +1,72 @@ +/* + * 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; + +/** + * 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. + */ + 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. + */ + 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(); + + /** + * Stops timing and disregards current timing data. + */ + void abort(); + + /** + * Used internally to get the actual backing Handler in the case of delegated Handlers + * + * @return TimingHandler + */ + TimingHandler getTimingHandler(); + + @Override + void close(); +} diff --git a/src/main/java/co/aikar/timings/TimingData.java b/src/main/java/co/aikar/timings/TimingData.java new file mode 100644 index 0000000..f222d6b --- /dev/null +++ b/src/main/java/co/aikar/timings/TimingData.java @@ -0,0 +1,120 @@ +/* + * This file is licensed under the MIT License (MIT). + * + * Copyright (c) 2014 Daniel Ennis + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package co.aikar.timings; + +import java.util.List; + +import static co.aikar.util.JSONUtil.toArray; + +/** + *

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); + } + + 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 0000000..916b6f9 --- /dev/null +++ b/src/main/java/co/aikar/timings/TimingHandler.java @@ -0,0 +1,209 @@ +/* + * 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 org.bukkit.Bukkit; + +import java.util.logging.Level; + +class TimingHandler implements Timing { + + private static int idPool = 1; + final int id = idPool++; + + final String name; + private final boolean verbose; + + private final Int2ObjectOpenHashMap children = new LoadingIntMap<>(TimingData::new); + + final TimingData record; + private final TimingHandler groupHandler; + + private long start = 0; + private int timingDepth = 0; + private boolean added; + private boolean timed; + private boolean enabled; + private TimingHandler parent; + + TimingHandler(TimingIdentifier id) { + if (id.name.startsWith("##")) { + verbose = true; + this.name = id.name.substring(3); + } else { + this.name = id.name; + verbose = false; + } + + this.record = new TimingData(this.id); + this.groupHandler = id.groupHandler; + + TimingIdentifier.getGroup(id.group).handlers.add(this); + checkEnabled(); + } + + final void checkEnabled() { + enabled = Timings.timingsEnabled && (!verbose || Timings.verboseEnabled); + } + + void processTick(boolean violated) { + if (timingDepth != 0 || record.getCurTickCount() == 0) { + timingDepth = 0; + start = 0; + return; + } + + record.processTick(violated); + for (TimingData handler : children.values()) { + handler.processTick(violated); + } + } + + @Override + public Timing startTimingIfSync() { + if (Bukkit.isPrimaryThread()) { + startTiming(); + } + return this; + } + + @Override + public void stopTimingIfSync() { + if (Bukkit.isPrimaryThread()) { + stopTiming(); + } + } + + public Timing startTiming() { + if (enabled && ++timingDepth == 1) { + start = System.nanoTime(); + parent = TimingsManager.CURRENT; + TimingsManager.CURRENT = this; + } + return this; + } + + public void stopTiming() { + if (enabled && --timingDepth == 0 && start != 0) { + if (!Bukkit.isPrimaryThread()) { + Bukkit.getLogger().log(Level.SEVERE, "stopTiming called async for " + name); + new Throwable().printStackTrace(); + start = 0; + return; + } + addDiff(System.nanoTime() - start); + start = 0; + } + } + + @Override + public void abort() { + if (enabled && timingDepth > 0) { + start = 0; + } + } + + void addDiff(long diff) { + if (TimingsManager.CURRENT == this) { + TimingsManager.CURRENT = parent; + if (parent != null) { + parent.children.get(id).add(diff); + } + } + record.add(diff); + if (!added) { + added = true; + timed = true; + TimingsManager.HANDLERS.add(this); + } + if (groupHandler != null) { + groupHandler.addDiff(diff); + groupHandler.children.get(id).add(diff); + } + } + + /** + * Reset this timer, setting all values to zero. + * + * @param full + */ + void reset(boolean full) { + record.reset(); + if (full) { + timed = false; + } + start = 0; + timingDepth = 0; + added = false; + children.clear(); + checkEnabled(); + } + + @Override + public TimingHandler getTimingHandler() { + return this; + } + + @Override + public boolean equals(Object o) { + return (this == o); + } + + @Override + public int hashCode() { + return id; + } + + /** + * This is simply for the Closeable interface so it can be used with + * try-with-resources () + */ + @Override + public void close() { + stopTimingIfSync(); + } + + public boolean isSpecial() { + return this == TimingsManager.FULL_SERVER_TICK || this == TimingsManager.TIMINGS_TICK; + } + + boolean isTimed() { + return timed; + } + + public boolean isEnabled() { + return enabled; + } + + TimingData[] cloneChildren() { + final TimingData[] clonedChildren = new TimingData[children.size()]; + int i = 0; + for (TimingData child : children.values()) { + clonedChildren[i++] = child.clone(); + } + return clonedChildren; + } +} diff --git a/src/main/java/co/aikar/timings/TimingHistory.java b/src/main/java/co/aikar/timings/TimingHistory.java new file mode 100644 index 0000000..389875b --- /dev/null +++ b/src/main/java/co/aikar/timings/TimingHistory.java @@ -0,0 +1,342 @@ +/* + * 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 co.aikar.util.JSONUtil; +import com.google.common.base.Function; +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; +import org.bukkit.Bukkit; +import org.bukkit.Chunk; +import org.bukkit.Material; +import org.bukkit.World; +import org.bukkit.block.BlockState; +import org.bukkit.entity.Entity; +import org.bukkit.entity.EntityType; +import org.bukkit.entity.Player; +import co.aikar.util.LoadingMap; +import co.aikar.util.MRUMapCache; + +import java.lang.management.ManagementFactory; +import java.util.Collection; +import java.util.EnumMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static co.aikar.timings.TimingsManager.FULL_SERVER_TICK; +import static co.aikar.timings.TimingsManager.MINUTE_REPORTS; +import static co.aikar.util.JSONUtil.*; + +@SuppressWarnings({"deprecation", "SuppressionAnnotation", "Convert2Lambda", "Anonymous2MethodRef"}) +public class TimingHistory { + public static long lastMinuteTime; + public static long timedTicks; + public static long playerTicks; + public static long entityTicks; + public static long tileEntityTicks; + public static long activatedEntityTicks; + private static int worldIdPool = 1; + static Map worldMap = LoadingMap.newHashMap(new Function() { + @Override + public Integer apply(String input) { + return worldIdPool++; + } + }); + private final long endTime; + private final long startTime; + private final long totalTicks; + private final long totalTime; // Represents all time spent running the server this history + private final MinuteReport[] minuteReports; + + private final TimingHistoryEntry[] entries; + final Set 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() { + @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()) { + data.entityCounts.get(entity.getType()).increment(); + } + + for (BlockState tileEntity : chunk.getTileEntities()) { + data.tileEntityCounts.get(tileEntity.getBlock().getType()).increment(); + } + } + return pair( + worldMap.get(world.getName()), + toArrayMapper(regions.values(),new Function() { + @Override + public Object apply(RegionData input) { + return toArray( + input.regionId.x, + input.regionId.z, + toObjectMapper(input.entityCounts.entrySet(), + new Function, JSONPair>() { + @Override + public JSONPair apply(Map.Entry entry) { + entityTypeSet.add(entry.getKey()); + return pair( + String.valueOf(entry.getKey().getTypeId()), + entry.getValue().count() + ); + } + } + ), + toObjectMapper(input.tileEntityCounts.entrySet(), + new Function, JSONPair>() { + @Override + public JSONPair apply(Map.Entry entry) { + tileEntityTypeSet.add(entry.getKey()); + return pair( + String.valueOf(entry.getKey().getId()), + entry.getValue().count() + ); + } + } + ) + ); + } + }) + ); + } + }); + } + static class RegionData { + final RegionId regionId; + @SuppressWarnings("Guava") + static Function LOADER = new Function() { + @Override + public RegionData apply(RegionId id) { + return new RegionData(id); + } + }; + RegionData(RegionId id) { + this.regionId = id; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + RegionData that = (RegionData) o; + + return regionId.equals(that.regionId); + + } + + @Override + public int hashCode() { + return regionId.hashCode(); + } + + @SuppressWarnings("unchecked") + final Map entityCounts = MRUMapCache.of(LoadingMap.of( + new EnumMap(EntityType.class), Counter.LOADER + )); + @SuppressWarnings("unchecked") + final Map tileEntityCounts = MRUMapCache.of(LoadingMap.of( + new EnumMap(Material.class), Counter.LOADER + )); + + static class RegionId { + final int x, z; + final long regionId; + RegionId(int x, int z) { + this.x = x >> 5 << 5; + this.z = z >> 5 << 5; + this.regionId = ((long) (this.x) << 32) + (this.z >> 5 << 5) - Integer.MIN_VALUE; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + RegionId regionId1 = (RegionId) o; + + return regionId == regionId1.regionId; + + } + + @Override + public int hashCode() { + return (int) (regionId ^ (regionId >>> 32)); + } + } + } + static void resetTicks(boolean fullReset) { + if (fullReset) { + // Non full is simply for 1 minute reports + timedTicks = 0; + } + lastMinuteTime = System.nanoTime(); + playerTicks = 0; + tileEntityTicks = 0; + entityTicks = 0; + activatedEntityTicks = 0; + } + + Object export() { + return createObject( + pair("s", startTime), + pair("e", endTime), + pair("tk", totalTicks), + pair("tm", totalTime), + pair("w", worlds), + pair("h", toArrayMapper(entries, new Function() { + @Override + public Object apply(TimingHistoryEntry entry) { + TimingData record = entry.data; + if (!record.hasData()) { + return null; + } + return entry.export(); + } + })), + pair("mp", toArrayMapper(minuteReports, new Function() { + @Override + public Object apply(MinuteReport input) { + return input.export(); + } + })) + ); + } + + static class MinuteReport { + final long time = System.currentTimeMillis() / 1000; + + final TicksRecord ticksRecord = new TicksRecord(); + final PingRecord pingRecord = new PingRecord(); + final TimingData fst = TimingsManager.FULL_SERVER_TICK.minuteData.clone(); + final double tps = 1E9 / ( System.nanoTime() - lastMinuteTime ) * ticksRecord.timed; + final double usedMemory = TimingsManager.FULL_SERVER_TICK.avgUsedMemory; + final double freeMemory = TimingsManager.FULL_SERVER_TICK.avgFreeMemory; + final double loadAvg = ManagementFactory.getOperatingSystemMXBean().getSystemLoadAverage(); + + List 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; + @SuppressWarnings({"rawtypes", "SuppressionAnnotation", "Guava"}) + static Function LOADER = new LoadingMap.Feeder() { + @Override + public Counter apply() { + return new Counter(); + } + }; + 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 0000000..0e114eb --- /dev/null +++ b/src/main/java/co/aikar/timings/TimingHistoryEntry.java @@ -0,0 +1,55 @@ +/* + * This file is licensed under the MIT License (MIT). + * + * Copyright (c) 2014 Daniel Ennis + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package co.aikar.timings; + +import com.google.common.base.Function; + +import java.util.List; + +import static co.aikar.util.JSONUtil.toArrayMapper; + +class TimingHistoryEntry { + final TimingData data; + private final TimingData[] children; + + TimingHistoryEntry(TimingHandler handler) { + this.data = handler.record.clone(); + children = handler.cloneChildren(); + } + + List export() { + List result = data.export(); + if (children.length > 0) { + result.add( + toArrayMapper(children, new Function() { + @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 0000000..623dda4 --- /dev/null +++ b/src/main/java/co/aikar/timings/TimingIdentifier.java @@ -0,0 +1,102 @@ +/* + * 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 co.aikar.util.LoadingMap; +import co.aikar.util.MRUMapCache; + +import java.util.ArrayDeque; +import java.util.Map; + +/** + *

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 = MRUMapCache.of( + LoadingMap.newIdentityHashMap(new Function() { + @Override + public TimingGroup apply(String group) { + return new TimingGroup(group); + } + }, 64) + ); + static final TimingGroup DEFAULT_GROUP = getGroup("Minecraft"); + final String group; + final String name; + final TimingHandler groupHandler; + final boolean protect; + private final int hashCode; + + TimingIdentifier(String group, String name, Timing groupHandler, boolean protect) { + this.group = group != null ? group.intern() : DEFAULT_GROUP.name; + this.name = name.intern(); + this.groupHandler = groupHandler != null ? groupHandler.getTimingHandler() : null; + this.protect = protect; + this.hashCode = (31 * this.group.hashCode()) + this.name.hashCode(); + } + + static TimingGroup getGroup(String groupName) { + if (groupName == null) { + return DEFAULT_GROUP; + } + + return GROUP_MAP.get(groupName.intern()); + } + + // We are using .intern() on the strings so it is guaranteed to be an identity comparison. + @SuppressWarnings("StringEquality") + @Override + public boolean equals(Object o) { + if (o == null) { + return false; + } + + TimingIdentifier that = (TimingIdentifier) o; + return group == that.group && name == that.name; + } + + @Override + public int hashCode() { + return hashCode; + } + + static class TimingGroup { + + private static int idPool = 1; + final int id = idPool++; + + final String name; + ArrayDeque handlers = new ArrayDeque(64); + + private TimingGroup(String name) { + this.name = name; + } + } +} 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 0000000..4e5cf35 --- /dev/null +++ b/src/main/java/co/aikar/timings/Timings.java @@ -0,0 +1,273 @@ +/* + * 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.bukkit.Bukkit; +import org.bukkit.command.CommandSender; +import org.bukkit.plugin.Plugin; + +import java.util.Queue; +import java.util.logging.Level; + +@SuppressWarnings("UnusedDeclaration") +public final class Timings { + + private static final int MAX_HISTORY_FRAMES = 12; + public static final Timing NULL_HANDLER = new NullTimingHandler(); + static boolean timingsEnabled = false; + static boolean verboseEnabled = false; + private static int historyInterval = -1; + private static int historyLength = -1; + + private Timings() {} + + /** + * Returns a Timing for a plugin corresponding to a name. + * + * @param plugin Plugin to own the Timing + * @param name Name of Timing + * @return Handler + */ + public static Timing of(Plugin plugin, String name) { + Timing pluginHandler = null; + if (plugin != null) { + pluginHandler = ofSafe(plugin.getName(), "Combined Total", TimingsManager.PLUGIN_GROUP_HANDLER); + } + return of(plugin, name, pluginHandler); + } + + /** + *

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 + */ + public static Timing of(Plugin plugin, String name, Timing groupHandler) { + Preconditions.checkNotNull(plugin, "Plugin can not be null"); + return TimingsManager.getHandler(plugin.getName(), name, groupHandler, true); + } + + /** + * Returns a Timing object after starting it, useful for Java7 try-with-resources. + * + * try (Timing ignored = Timings.ofStart(plugin, someName)) { + * // timed section + * } + * + * @param plugin Plugin to own the Timing + * @param name Name of Timing + * @return Timing Handler + */ + public static Timing ofStart(Plugin plugin, String name) { + return ofStart(plugin, name, null); + } + + /** + * Returns a Timing object after starting it, useful for Java7 try-with-resources. + * + * try (Timing ignored = Timings.ofStart(plugin, someName, groupHandler)) { + * // timed section + * } + * + * @param plugin Plugin to own the Timing + * @param name Name of Timing + * @param groupHandler Parent handler to mirror .start/stop calls to + * @return Timing Handler + */ + public static Timing ofStart(Plugin plugin, String name, Timing groupHandler) { + Timing timing = of(plugin, name, groupHandler); + timing.startTimingIfSync(); + 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(CommandSender sender) { + if (sender == null) { + sender = Bukkit.getConsoleSender(); + } + TimingsExport.reportTimings(sender); + } + + /* + ================= + Protected API: These are for internal use only in Bukkit/CraftBukkit + These do not have isPrimaryThread() checks in the startTiming/stopTiming + ================= + */ + + static TimingHandler ofSafe(String name) { + return ofSafe(null, name, null); + } + + static Timing ofSafe(Plugin plugin, String name) { + Timing pluginHandler = null; + if (plugin != null) { + pluginHandler = ofSafe(plugin.getName(), "Combined Total", TimingsManager.PLUGIN_GROUP_HANDLER); + } + return ofSafe(plugin != null ? plugin.getName() : "Minecraft - Invalid Plugin", name, pluginHandler); + } + + static TimingHandler ofSafe(String name, Timing groupHandler) { + return ofSafe(null, name, groupHandler); + } + + static TimingHandler ofSafe(String groupName, String name, Timing groupHandler) { + return TimingsManager.getHandler(groupName, name, groupHandler, false); + } +} 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 0000000..1fa0eb5 --- /dev/null +++ b/src/main/java/co/aikar/timings/TimingsCommand.java @@ -0,0 +1,119 @@ +/* + * This file is licensed under the MIT License (MIT). + * + * Copyright (c) 2014 Daniel Ennis + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package co.aikar.timings; + +import com.google.common.collect.ImmutableList; +import org.apache.commons.lang.Validate; +import org.bukkit.ChatColor; +import org.bukkit.command.CommandSender; +import org.bukkit.command.defaults.BukkitCommand; +import org.bukkit.util.StringUtil; + +import java.util.ArrayList; +import java.util.List; + + +public class TimingsCommand extends BukkitCommand { + private static final List TIMINGS_SUBCOMMANDS = ImmutableList.of("report", "reset", "on", "off", "paste", "verbon", "verboff"); + private long lastResetAttempt = 0; + + public TimingsCommand(String name) { + super(name); + this.description = "Manages Spigot Timings data to see performance of the server."; + this.usageMessage = "/timings "; + this.setPermission("bukkit.command.timings"); + } + + @Override + public boolean execute(CommandSender sender, String currentAlias, String[] args) { + if (!testPermission(sender)) { + return true; + } + if (args.length < 1) { + sender.sendMessage(ChatColor.RED + "Usage: " + usageMessage); + return true; + } + final String arg = args[0]; + if ("on".equalsIgnoreCase(arg)) { + Timings.setTimingsEnabled(true); + sender.sendMessage("Enabled Timings & Reset"); + return true; + } else if ("off".equalsIgnoreCase(arg)) { + Timings.setTimingsEnabled(false); + sender.sendMessage("Disabled Timings"); + return true; + } + + if (!Timings.isTimingsEnabled()) { + sender.sendMessage("Please enable timings by typing /timings on"); + return true; + } + + long now = System.currentTimeMillis(); + if ("verbon".equalsIgnoreCase(arg)) { + Timings.setVerboseTimingsEnabled(true); + sender.sendMessage("Enabled Verbose Timings"); + return true; + } else if ("verboff".equalsIgnoreCase(arg)) { + Timings.setVerboseTimingsEnabled(false); + sender.sendMessage("Disabled Verbose Timings"); + return true; + } else if ("reset".equalsIgnoreCase(arg)) { + if (now - lastResetAttempt < 30000) { + TimingsManager.reset(); + sender.sendMessage(ChatColor.RED + "Timings reset. Please wait 5-10 minutes before using /timings report."); + } else { + lastResetAttempt = now; + sender.sendMessage(ChatColor.RED + "WARNING: Timings v2 should not be reset. If you are encountering lag, please wait 3 minutes and then issue a report. The best timings will include 10+ minutes, with data before and after your lag period. If you really want to reset, run this command again within 30 seconds."); + } + + } else if ("cost".equals(arg)) { + sender.sendMessage("Timings cost: " + TimingsExport.getCost()); + } else if ( + "paste".equalsIgnoreCase(arg) || + "report".equalsIgnoreCase(arg) || + "get".equalsIgnoreCase(arg) || + "merged".equalsIgnoreCase(arg) || + "separate".equalsIgnoreCase(arg) + ) { + TimingsExport.reportTimings(sender); + } else { + sender.sendMessage(ChatColor.RED + "Usage: " + usageMessage); + } + return true; + } + + @Override + public List tabComplete(CommandSender sender, String alias, String[] args) { + Validate.notNull(sender, "Sender cannot be null"); + Validate.notNull(args, "Arguments cannot be null"); + Validate.notNull(alias, "Alias cannot be null"); + + if (args.length == 1) { + return StringUtil.copyPartialMatches(args[0], TIMINGS_SUBCOMMANDS, + new ArrayList(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 0000000..a4a7fea --- /dev/null +++ b/src/main/java/co/aikar/timings/TimingsExport.java @@ -0,0 +1,388 @@ +/* + * 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 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.command.ConsoleCommandSender; +import org.bukkit.command.RemoteConsoleCommandSender; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.MemorySection; +import org.bukkit.entity.EntityType; +import org.bukkit.plugin.Plugin; +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.GarbageCollectorMXBean; +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.*; + +@SuppressWarnings({"rawtypes", "SuppressionAnnotation"}) +class TimingsExport extends Thread { + + private final CommandSender sender; + private final Map out; + private final TimingHistory[] history; + private static long lastReport = 0; + + private TimingsExport(CommandSender sender, Map out, TimingHistory[] history) { + super("Timings paste thread"); + this.sender = sender; + this.out = out; + this.history = history; + } + + /** + * Builds an XML report of the timings to be uploaded for parsing. + * + * @param sender Who to report to + */ + static void reportTimings(CommandSender sender) { + long now = System.currentTimeMillis(); + final long lastReportDiff = now - lastReport; + if (lastReportDiff < 60000) { + sender.sendMessage(ChatColor.RED + "Please wait at least 1 minute in between Timings reports. (" + (int)((60000 - lastReportDiff) / 1000) + " seconds)"); + return; + } + final long lastStartDiff = now - TimingsManager.timingStart; + if (lastStartDiff < 180000) { + sender.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)"); + return; + } + lastReport = now; + Map parent = createObject( + // Get some basic system details about the server + pair("version", Bukkit.getVersion()), + pair("maxplayers", Bukkit.getMaxPlayers()), + pair("start", TimingsManager.timingStart / 1000), + pair("end", System.currentTimeMillis() / 1000), + pair("sampletime", (System.currentTimeMillis() - TimingsManager.timingStart) / 1000) + ); + if (!TimingsManager.privacy) { + appendObjectData(parent, + pair("server", Bukkit.getServerName()), + pair("motd", Bukkit.getServer().getMotd()), + pair("online-mode", Bukkit.getServer().getOnlineMode()), + pair("icon", Bukkit.getServer().getServerIcon().getData()) + ); + } + + final Runtime runtime = Runtime.getRuntime(); + RuntimeMXBean runtimeBean = ManagementFactory.getRuntimeMXBean(); + + parent.put("system", createObject( + pair("timingcost", getCost()), + pair("name", System.getProperty("os.name")), + pair("version", System.getProperty("os.version")), + pair("jvmversion", System.getProperty("java.version")), + pair("arch", System.getProperty("os.arch")), + pair("maxmem", runtime.maxMemory()), + pair("cpu", runtime.availableProcessors()), + pair("runtime", ManagementFactory.getRuntimeMXBean().getUptime()), + pair("flags", StringUtils.join(runtimeBean.getInputArguments(), " ")), + pair("gc", toObjectMapper(ManagementFactory.getGarbageCollectorMXBeans(), new Function() { + @Override + public JSONPair apply(GarbageCollectorMXBean input) { + return 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(); + for (TimingIdentifier.TimingGroup group : TimingIdentifier.GROUP_MAP.values()) { + for (TimingHandler id : group.handlers) { + if (!id.isTimed() && !id.isSpecial()) { + continue; + } + handlers.put(id.id, toArray( + group.id, + id.name + )); + } + } + + parent.put("idmap", createObject( + pair("groups", toObjectMapper( + TimingIdentifier.GROUP_MAP.values(), new Function() { + @Override + public JSONPair apply(TimingIdentifier.TimingGroup group) { + return pair(group.id, group.name); + } + })), + pair("handlers", handlers), + pair("worlds", toObjectMapper(TimingHistory.worldMap.entrySet(), new Function, JSONPair>() { + @Override + public JSONPair apply(Map.Entry input) { + return pair(input.getValue(), input.getKey()); + } + })), + pair("tileentity", + toObjectMapper(tileEntityTypeSet, new Function() { + @Override + public JSONPair apply(Material input) { + return pair(input.getId(), input.name()); + } + })), + pair("entity", + toObjectMapper(entityTypeSet, new Function() { + @Override + public JSONPair apply(EntityType input) { + return pair(input.getTypeId(), input.name()); + } + })) + )); + + // Information about loaded plugins + + parent.put("plugins", toObjectMapper(Bukkit.getPluginManager().getPlugins(), + new Function() { + @Override + public JSONPair apply(Plugin plugin) { + return 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(sender, 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, new Function() { + @Override + public Object apply(Object input) { + return valAsJSON(input, parentKey); + } + }); + } else { + return val.toString(); + } + } else { + return mapAsJSON((ConfigurationSection) val, parentKey); + } + } + + @SuppressWarnings("CallToThreadRun") + @Override + public synchronized void start() { + if (sender instanceof RemoteConsoleCommandSender) { + sender.sendMessage(ChatColor.RED + "Warning: Timings report done over RCON will cause lag spikes."); + sender.sendMessage(ChatColor.RED + "You should use " + ChatColor.YELLOW + + "/timings report" + ChatColor.RED + " in game or console."); + run(); + } else { + super.start(); + } + } + + @Override + public void run() { + sender.sendMessage(ChatColor.GREEN + "Preparing Timings Report..."); + + + out.put("data", toArrayMapper(history, new Function() { + @Override + public Object apply(TimingHistory input) { + return input.export(); + } + })); + + + String response = null; + try { + HttpURLConnection con = (HttpURLConnection) new URL("http://timings.aikar.co/post").openConnection(); + con.setDoOutput(true); + String hostName = "BrokenHost"; + try { + hostName = InetAddress.getLocalHost().getHostName(); + } catch(Exception ignored) {} + con.setRequestProperty("User-Agent", "Paper/" + Bukkit.getServerName() + "/" + hostName); + con.setRequestMethod("POST"); + con.setInstanceFollowRedirects(false); + + OutputStream request = new GZIPOutputStream(con.getOutputStream()) {{ + this.def.setLevel(7); + }}; + + request.write(JSONValue.toJSONString(out).getBytes("UTF-8")); + request.close(); + + response = getResponse(con); + + if (con.getResponseCode() != 302) { + sender.sendMessage( + ChatColor.RED + "Upload Error: " + con.getResponseCode() + ": " + con.getResponseMessage()); + sender.sendMessage(ChatColor.RED + "Check your logs for more information"); + if (response != null) { + Bukkit.getLogger().log(Level.SEVERE, response); + } + return; + } + + String location = con.getHeaderField("Location"); + sender.sendMessage(ChatColor.GREEN + "View Timings Report: " + location); + if (!(sender instanceof ConsoleCommandSender)) { + Bukkit.getLogger().log(Level.INFO, "View Timings Report: " + location); + } + + if (response != null && !response.isEmpty()) { + Bukkit.getLogger().log(Level.INFO, "Timing Response: " + response); + } + } catch (IOException ex) { + sender.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); + } + } + + 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) { + sender.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 0000000..58ed35e --- /dev/null +++ b/src/main/java/co/aikar/timings/TimingsManager.java @@ -0,0 +1,196 @@ +/* + * 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 com.google.common.collect.EvictingQueue; +import org.bukkit.Bukkit; +import org.bukkit.Server; +import org.bukkit.command.Command; +import org.bukkit.plugin.Plugin; +import org.bukkit.plugin.java.PluginClassLoader; +import co.aikar.util.LoadingMap; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.logging.Level; + +public final class TimingsManager { + static final Map TIMING_MAP = + Collections.synchronizedMap(LoadingMap.newHashMap( + new Function() { + @Override + public TimingHandler apply(TimingIdentifier id) { + return (id.protect ? + new UnsafeTimingHandler(id) : + new TimingHandler(id) + ); + } + }, + 256, .5F + )); + 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 Collection HANDLERS = new ArrayDeque(); + static final ArrayDeque MINUTE_REPORTS = new ArrayDeque(); + + static EvictingQueue HISTORY = EvictingQueue.create(12); + static TimingHandler CURRENT; + static long timingStart = 0; + static long historyStart = 0; + static boolean needsFullReset = false; + static boolean needsRecheckEnabled = false; + + private TimingsManager() {} + + /** + * Resets all timing data on the next tick + */ + static void reset() { + needsFullReset = true; + } + + /** + * Ticked every tick by CraftBukkit to count the number of times a timer + * caused TPS loss. + */ + static void tick() { + if (Timings.timingsEnabled) { + boolean violated = FULL_SERVER_TICK.isViolated(); + + for (TimingHandler handler : HANDLERS) { + if (handler.isSpecial()) { + // We manually call this + continue; + } + handler.processTick(violated); + } + + TimingHistory.playerTicks += Bukkit.getOnlinePlayers().size(); + TimingHistory.timedTicks++; + // Generate TPS/Ping/Tick reports every minute + } + } + static void stopServer() { + Timings.timingsEnabled = false; + recheckEnabled(); + } + static void recheckEnabled() { + synchronized (TIMING_MAP) { + for (TimingHandler timings : TIMING_MAP.values()) { + timings.checkEnabled(); + } + } + needsRecheckEnabled = false; + } + static void resetTimings() { + if (needsFullReset) { + // Full resets need to re-check every handlers enabled state + // Timing map can be modified from async so we must sync on it. + synchronized (TIMING_MAP) { + for (TimingHandler timings : TIMING_MAP.values()) { + timings.reset(true); + } + } + Bukkit.getLogger().log(Level.INFO, "Timings Reset"); + HISTORY.clear(); + needsFullReset = false; + needsRecheckEnabled = false; + timingStart = System.currentTimeMillis(); + } else { + // Soft resets only need to act on timings that have done something + // Handlers can only be modified on main thread. + for (TimingHandler timings : HANDLERS) { + timings.reset(false); + } + } + + HANDLERS.clear(); + MINUTE_REPORTS.clear(); + + TimingHistory.resetTicks(true); + historyStart = System.currentTimeMillis(); + } + + static TimingHandler getHandler(String group, String name, Timing parent, boolean protect) { + return TIMING_MAP.get(new TimingIdentifier(group, name, parent, protect)); + } + + + /** + *

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 + */ + public static Timing getCommandTiming(String pluginName, Command command) { + Plugin plugin = null; + final Server server = Bukkit.getServer(); + if (!( server == null || pluginName == null || + "minecraft".equals(pluginName) || "bukkit".equals(pluginName) || + "spigot".equalsIgnoreCase(pluginName) || "paper".equals(pluginName) + )) { + plugin = server.getPluginManager().getPlugin(pluginName); + } + if (plugin == null) { + // Plugin is passing custom fallback prefix, try to look up by class loader + plugin = getPluginByClassloader(command.getClass()); + } + if (plugin == null) { + return Timings.ofSafe("Command: " + pluginName + ":" + command.getTimingName()); + } + + return Timings.ofSafe(plugin, "Command: " + pluginName + ":" + command.getTimingName()); + } + + /** + * Looks up the class loader for the specified class, and if it is a PluginClassLoader, return the + * Plugin that created this class. + * + * @param clazz Class to check + * @return Plugin if created by a plugin + */ + public static Plugin getPluginByClassloader(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/UnsafeTimingHandler.java b/src/main/java/co/aikar/timings/UnsafeTimingHandler.java new file mode 100644 index 0000000..5edaba1 --- /dev/null +++ b/src/main/java/co/aikar/timings/UnsafeTimingHandler.java @@ -0,0 +1,51 @@ +/* + * This file is licensed under the MIT License (MIT). + * + * Copyright (c) 2014 Daniel Ennis + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package co.aikar.timings; + +import org.bukkit.Bukkit; + +class UnsafeTimingHandler extends TimingHandler { + + UnsafeTimingHandler(TimingIdentifier id) { + super(id); + } + + private static void checkThread() { + if (!Bukkit.isPrimaryThread()) { + throw new IllegalStateException("Calling Timings from Async Operation"); + } + } + + @Override + public Timing startTiming() { + checkThread(); + return super.startTiming(); + } + + @Override + public void stopTiming() { + checkThread(); + super.stopTiming(); + } +} diff --git a/src/main/java/co/aikar/util/JSONUtil.java b/src/main/java/co/aikar/util/JSONUtil.java new file mode 100644 index 0000000..5fdf7c4 --- /dev/null +++ b/src/main/java/co/aikar/util/JSONUtil.java @@ -0,0 +1,123 @@ +package co.aikar.util; + +import com.google.common.base.Function; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Provides Utility methods that assist with generating JSON Objects + */ +@SuppressWarnings({"rawtypes", "SuppressionAnnotation"}) +public final class JSONUtil { + private JSONUtil() {} + + /** + * Creates a key/value "JSONPair" object + * @param key + * @param obj + * @return + */ + public static JSONPair pair(String key, Object obj) { + return new JSONPair(key, obj); + } + + public static JSONPair pair(long key, Object obj) { + return new JSONPair(String.valueOf(key), obj); + } + + /** + * Creates a new JSON object from multiple JsonPair key/value pairs + * @param data + * @return + */ + public static Map createObject(JSONPair... data) { + return appendObjectData(new LinkedHashMap(), data); + } + + /** + * This appends multiple key/value Obj pairs into a JSON Object + * @param parent + * @param data + * @return + */ + public static Map appendObjectData(Map parent, JSONPair... data) { + for (JSONPair JSONPair : data) { + parent.put(JSONPair.key, JSONPair.val); + } + return parent; + } + + /** + * This builds a JSON array from a set of data + * @param data + * @return + */ + public static List toArray(Object... data) { + return Lists.newArrayList(data); + } + + /** + * These help build a single JSON array using a mapper function + * @param collection + * @param mapper + * @param + * @return + */ + public static List toArrayMapper(E[] collection, Function mapper) { + return toArrayMapper(Lists.newArrayList(collection), mapper); + } + + public static List toArrayMapper(Iterable collection, 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 + * @param mapper + * @param + * @return + */ + public static Map toObjectMapper(E[] collection, Function mapper) { + return toObjectMapper(Lists.newArrayList(collection), mapper); + } + + public static Map toObjectMapper(Iterable collection, 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(String key, Object val) { + this.key = key; + this.val = val; + } + } +} diff --git a/src/main/java/co/aikar/util/LoadingIntMap.java b/src/main/java/co/aikar/util/LoadingIntMap.java new file mode 100644 index 0000000..79fa9d5 --- /dev/null +++ b/src/main/java/co/aikar/util/LoadingIntMap.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2015. Starlis LLC / dba Empire Minecraft + * + * This source code is proprietary software and must not be redistributed without Starlis LLC's approval + * + */ +package co.aikar.util; + + +import com.google.common.base.Function; +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; + +/** + * Allows you to pass a Loader function that when a key is accessed that doesn't exist, + * automatically loads the entry into the map by calling the loader Function. + * + * .get() Will only return null if the Loader can return null. + * + * You may pass any backing Map to use. + * + * This class is not thread safe and should be wrapped with Collections.synchronizedMap on the OUTSIDE of the LoadingMap if needed. + * + * Do not wrap the backing map with Collections.synchronizedMap. + * + * @param Value + */ +public class LoadingIntMap extends Int2ObjectOpenHashMap { + private final Function loader; + + public LoadingIntMap(Function loader) { + super(); + this.loader = loader; + } + + public LoadingIntMap(int expectedSize, Function loader) { + super(expectedSize); + this.loader = loader; + } + + public LoadingIntMap(int expectedSize, float loadFactor, Function loader) { + super(expectedSize, loadFactor); + this.loader = loader; + } + + + @Override + public V get(int key) { + V res = super.get(key); + if (res == null) { + res = loader.apply(key); + if (res != null) { + put(key, res); + } + } + return res; + } + + /** + * Due to java stuff, you will need to cast it to (Function) for some cases + * @param + */ + public abstract static class Feeder implements Function { + @Override + public T apply(Object input) { + return apply(); + } + + public abstract T apply(); + } +} diff --git a/src/main/java/co/aikar/util/LoadingMap.java b/src/main/java/co/aikar/util/LoadingMap.java new file mode 100644 index 0000000..a9f2919 --- /dev/null +++ b/src/main/java/co/aikar/util/LoadingMap.java @@ -0,0 +1,332 @@ +/* + * 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.Function; +import org.bukkit.Material; +import co.aikar.timings.TimingHistory; +import org.w3c.dom.css.Counter; + +import java.lang.reflect.Constructor; +import java.util.AbstractMap; +import java.util.Collection; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.IdentityHashMap; +import java.util.Map; +import java.util.Set; + +/** + * 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 Function loader; + + /** + * Initializes an auto loading map using specified loader and backing map + * @param backingMap + * @param loader + */ + public LoadingMap(Map backingMap, Function loader) { + this.backingMap = backingMap; + this.loader = loader; + } + + /** + * Creates a new LoadingMap with the specified map and loader + * @param backingMap + * @param loader + * @param + * @param + * @return + */ + public static Map of(Map backingMap, 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() + */ + public static Map newAutoMap(Map backingMap, final Class keyClass, + 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() + */ + public static Map newAutoMap(Map backingMap, + final Class valueClass) { + return newAutoMap(backingMap, null, valueClass); + } + + /** + * @see #newAutoMap + * + * new Auto initializing map using a HashMap. + * @param keyClass + * @param valueClass + * @param + * @param + * @return + */ + public static Map newHashAutoMap(final Class keyClass, final Class valueClass) { + return newAutoMap(new HashMap(), keyClass, valueClass); + } + + /** + * @see #newAutoMap + * + * new Auto initializing map using a HashMap. + * @param valueClass + * @param + * @param + * @return + */ + public static Map newHashAutoMap(final Class valueClass) { + return newHashAutoMap(null, valueClass); + } + + /** + * @see #newAutoMap + * + * new Auto initializing map using a HashMap. + * + * @param keyClass + * @param valueClass + * @param initialCapacity + * @param loadFactor + * @param + * @param + * @return + */ + public static Map newHashAutoMap(final Class keyClass, 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 + * @param initialCapacity + * @param loadFactor + * @param + * @param + * @return + */ + public static Map newHashAutoMap(final Class valueClass, int initialCapacity, float loadFactor) { + return newHashAutoMap(null, valueClass, initialCapacity, loadFactor); + } + + /** + * Initializes an auto loading map using a HashMap + * @param loader + * @param + * @param + * @return + */ + public static Map newHashMap(Function loader) { + return new LoadingMap(new HashMap(), loader); + } + + /** + * Initializes an auto loading map using a HashMap + * @param loader + * @param initialCapacity + * @param loadFactor + * @param + * @param + * @return + */ + public static Map newHashMap(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 + * @param + * @param + * @return + */ + public static Map newIdentityHashMap(Function loader) { + return new LoadingMap(new IdentityHashMap(), loader); + } + + /** + * Initializes an auto loading map using an Identity HashMap + * @param loader + * @param initialCapacity + * @param + * @param + * @return + */ + public static Map newIdentityHashMap(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(Object key) {return backingMap.containsKey(key);} + + @Override + public boolean containsValue(Object value) {return backingMap.containsValue(value);} + + @Override + public V get(Object key) { + V res = backingMap.get(key); + if (res == null && key != null) { + res = loader.apply((K) key); + if (res != null) { + backingMap.put((K) key, res); + } + } + return res; + } + + public V put(K key, V value) {return backingMap.put(key, value);} + + @Override + public V remove(Object key) {return backingMap.remove(key);} + + public void putAll(Map m) {backingMap.putAll(m);} + + @Override + public void clear() {backingMap.clear();} + + @Override + public Set keySet() {return backingMap.keySet();} + + @Override + public Collection values() {return backingMap.values();} + + @Override + public boolean equals(Object o) {return backingMap.equals(o);} + + @Override + public int hashCode() {return backingMap.hashCode();} + + @Override + public Set> entrySet() { + return backingMap.entrySet(); + } + + public LoadingMap clone() { + return new LoadingMap(backingMap, loader); + } + + private static class AutoInstantiatingLoader implements Function { + final Constructor constructor; + private final Class valueClass; + + AutoInstantiatingLoader(Class keyClass, 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)); + } + } + + @Override + public V apply(K input) { + try { + return (constructor != null ? constructor.newInstance(input) : valueClass.newInstance()); + } catch (Exception e) { + throw new ExceptionInInitializerError(e); + } + } + + @Override + public int hashCode() { + return super.hashCode(); + } + + @Override + public boolean equals(Object object) { + return false; + } + } + + /** + * Due to java stuff, you will need to cast it to (Function) for some cases + * @param + */ + public abstract static class Feeder implements Function { + @Override + public T apply(Object input) { + return apply(); + } + + public abstract T apply(); + } +} diff --git a/src/main/java/co/aikar/util/MRUMapCache.java b/src/main/java/co/aikar/util/MRUMapCache.java new file mode 100644 index 0000000..3a288d2 --- /dev/null +++ b/src/main/java/co/aikar/util/MRUMapCache.java @@ -0,0 +1,100 @@ +/* + * 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; + +/** + * Implements a Most Recently Used cache in front of a backing map, to quickly access the last accessed result. + * @param + * @param + */ +public class MRUMapCache extends AbstractMap { + final Map backingMap; + Object cacheKey; + V cacheValue; + public MRUMapCache(final Map backingMap) { + this.backingMap = backingMap; + } + + public int size() {return backingMap.size();} + + public boolean isEmpty() {return backingMap.isEmpty();} + + public boolean containsKey(Object key) { + return key != null && key.equals(cacheKey) || backingMap.containsKey(key); + } + + public boolean containsValue(Object value) { + return value != null && value == cacheValue || backingMap.containsValue(value); + } + + public V get(Object key) { + if (cacheKey != null && cacheKey.equals(key)) { + return cacheValue; + } + cacheKey = key; + return cacheValue = backingMap.get(key); + } + + public V put(K key, V value) { + cacheKey = key; + return cacheValue = backingMap.put(key, value); + } + + public V remove(Object key) { + if (key != null && key.equals(cacheKey)) { + cacheKey = null; + } + return backingMap.remove(key); + } + + public void putAll(Map m) {backingMap.putAll(m);} + + public void clear() { + cacheKey = null; + cacheValue = null; + backingMap.clear(); + } + + public Set keySet() {return backingMap.keySet();} + + public Collection values() {return backingMap.values();} + + public Set> entrySet() {return backingMap.entrySet();} + + /** + * Wraps the specified map with a most recently used cache + * @param map + * @param + * @param + * @return + */ + public static Map of(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 5b6413b..7fd544e 100644 --- a/src/main/java/org/bukkit/Bukkit.java +++ b/src/main/java/org/bukkit/Bukkit.java @@ -538,7 +538,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 120dba2..77cfe56 100644 --- a/src/main/java/org/bukkit/Server.java +++ b/src/main/java/org/bukkit/Server.java @@ -943,12 +943,27 @@ public interface Server extends PluginMessageRecipient { public class Spigot { - + @Deprecated public org.bukkit.configuration.file.YamlConfiguration getConfig() { throw new UnsupportedOperationException( "Not supported yet." ); } + public org.bukkit.configuration.file.YamlConfiguration getBukkitConfig() + { + throw new UnsupportedOperationException( "Not supported yet." ); + } + + public org.bukkit.configuration.file.YamlConfiguration getSpigotConfig() + { + throw new UnsupportedOperationException("Not supported yet."); + } + + public org.bukkit.configuration.file.YamlConfiguration getPaperConfig() + { + throw new UnsupportedOperationException("Not supported yet."); + } + /** * Sends the component to the player * diff --git a/src/main/java/org/bukkit/command/Command.java b/src/main/java/org/bukkit/command/Command.java index 08a9739..347d218 100644 --- a/src/main/java/org/bukkit/command/Command.java +++ b/src/main/java/org/bukkit/command/Command.java @@ -32,7 +32,8 @@ public abstract class Command { protected String usageMessage; private String permission; private String permissionMessage; - public org.spigotmc.CustomTimingsHandler timings; // Spigot + public co.aikar.timings.Timing timings; // Spigot + public String getTimingName() {return getName();} // Spigot protected Command(String name) { this(name, "", "/" + name, new ArrayList()); @@ -46,7 +47,6 @@ public abstract class Command { this.usageMessage = usageMessage; this.aliases = aliases; this.activeAliases = new ArrayList(aliases); - this.timings = new org.spigotmc.CustomTimingsHandler("** Command: " + name); // Spigot } /** @@ -235,7 +235,6 @@ public abstract class Command { public boolean setLabel(String name) { this.nextLabel = name; if (!isRegistered()) { - this.timings = new org.spigotmc.CustomTimingsHandler("** Command: " + name); // Spigot this.label = name; return true; } diff --git a/src/main/java/org/bukkit/command/FormattedCommandAlias.java b/src/main/java/org/bukkit/command/FormattedCommandAlias.java index 3f07d7f..f89ad07 100644 --- a/src/main/java/org/bukkit/command/FormattedCommandAlias.java +++ b/src/main/java/org/bukkit/command/FormattedCommandAlias.java @@ -14,6 +14,7 @@ public class FormattedCommandAlias extends Command { public FormattedCommandAlias(String alias, String[] formatStrings) { super(alias); + timings = co.aikar.timings.TimingsManager.getCommandTiming("minecraft", this); // Spigot this.formatStrings = formatStrings; } @@ -118,6 +119,9 @@ public class FormattedCommandAlias extends Command { return formatString; } + @Override // Spigot + public String getTimingName() {return "Command Forwarder - " + super.getTimingName();} // Spigot + private static boolean inRange(int i, int j, int k) { return i >= j && i <= k; } diff --git a/src/main/java/org/bukkit/command/SimpleCommandMap.java b/src/main/java/org/bukkit/command/SimpleCommandMap.java index 5965514..466757b 100644 --- a/src/main/java/org/bukkit/command/SimpleCommandMap.java +++ b/src/main/java/org/bukkit/command/SimpleCommandMap.java @@ -31,7 +31,7 @@ public class SimpleCommandMap implements CommandMap { register("bukkit", new VersionCommand("version")); register("bukkit", new ReloadCommand("reload")); register("bukkit", new PluginsCommand("plugins")); - register("bukkit", new TimingsCommand("timings")); + register("bukkit", new co.aikar.timings.TimingsCommand("timings")); // Spigot } public void setFallbackCommands() { @@ -60,6 +60,7 @@ public class SimpleCommandMap implements CommandMap { * {@inheritDoc} */ public boolean register(String label, String fallbackPrefix, Command command) { + command.timings = co.aikar.timings.TimingsManager.getCommandTiming(fallbackPrefix, command); // Spigot label = label.toLowerCase(java.util.Locale.ENGLISH).trim(); fallbackPrefix = fallbackPrefix.toLowerCase(java.util.Locale.ENGLISH).trim(); boolean registered = register(label, command, false, fallbackPrefix); @@ -135,6 +136,12 @@ public class SimpleCommandMap implements CommandMap { return false; } + // Paper start - Plugins do weird things to workaround normal registration + if (target.timings == null) { + target.timings = co.aikar.timings.TimingsManager.getCommandTiming(null, target); + } + // Paper end + try { target.timings.startTiming(); // Spigot // Note: we don't return the result of target.execute as thats success / failure, we return handled (true) or not handled (false) diff --git a/src/main/java/org/bukkit/command/defaults/TimingsCommand.java b/src/main/java/org/bukkit/command/defaults/TimingsCommand.java deleted file mode 100644 index a8f61f2..0000000 --- a/src/main/java/org/bukkit/command/defaults/TimingsCommand.java +++ /dev/null @@ -1,253 +0,0 @@ -package org.bukkit.command.defaults; - -import java.io.File; -import java.io.IOException; -import java.io.PrintStream; -import java.util.ArrayList; -import java.util.List; - -import org.apache.commons.lang.Validate; -import org.bukkit.Bukkit; -import org.bukkit.ChatColor; -import org.bukkit.command.CommandSender; -import org.bukkit.event.Event; -import org.bukkit.event.HandlerList; -import org.bukkit.plugin.Plugin; -import org.bukkit.plugin.RegisteredListener; -import org.bukkit.plugin.TimedRegisteredListener; -import org.bukkit.util.StringUtil; - -import com.google.common.collect.ImmutableList; - -// Spigot start -import java.io.ByteArrayOutputStream; -import java.io.OutputStream; -import java.net.HttpURLConnection; -import java.net.URL; -import java.net.URLEncoder; -import java.util.logging.Level; - -import org.bukkit.command.RemoteConsoleCommandSender; -import org.bukkit.plugin.SimplePluginManager; -import org.spigotmc.CustomTimingsHandler; -// Spigot end - -public class TimingsCommand extends BukkitCommand { - private static final List TIMINGS_SUBCOMMANDS = ImmutableList.of("report", "reset", "on", "off", "paste"); // Spigot - public static long timingStart = 0; // Spigot - - public TimingsCommand(String name) { - super(name); - this.description = "Manages Spigot Timings data to see performance of the server."; // Spigot - this.usageMessage = "/timings "; // Spigot - this.setPermission("bukkit.command.timings"); - } - - // Spigot start - redesigned Timings Command - public void executeSpigotTimings(CommandSender sender, String[] args) { - if ( "on".equals( args[0] ) ) - { - ( (SimplePluginManager) Bukkit.getPluginManager() ).useTimings( true ); - CustomTimingsHandler.reload(); - sender.sendMessage( "Enabled Timings & Reset" ); - return; - } else if ( "off".equals( args[0] ) ) - { - ( (SimplePluginManager) Bukkit.getPluginManager() ).useTimings( false ); - sender.sendMessage( "Disabled Timings" ); - return; - } - - if ( !Bukkit.getPluginManager().useTimings() ) - { - sender.sendMessage( "Please enable timings by typing /timings on" ); - return; - } - - boolean paste = "paste".equals( args[0] ); - if ("reset".equals(args[0])) { - CustomTimingsHandler.reload(); - sender.sendMessage("Timings reset"); - } else if ("merged".equals(args[0]) || "report".equals(args[0]) || paste) { - long sampleTime = System.nanoTime() - timingStart; - int index = 0; - File timingFolder = new File("timings"); - timingFolder.mkdirs(); - File timings = new File(timingFolder, "timings.txt"); - ByteArrayOutputStream bout = ( paste ) ? new ByteArrayOutputStream() : null; - while (timings.exists()) timings = new File(timingFolder, "timings" + (++index) + ".txt"); - PrintStream fileTimings = null; - try { - fileTimings = ( paste ) ? new PrintStream( bout ) : new PrintStream( timings ); - - CustomTimingsHandler.printTimings(fileTimings); - fileTimings.println( "Sample time " + sampleTime + " (" + sampleTime / 1E9 + "s)" ); - - fileTimings.println( "" ); - 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(CommandSender sender, String currentAlias, String[] args) { - if (!testPermission(sender)) return true; - if (args.length < 1) { // Spigot - sender.sendMessage(ChatColor.RED + "Usage: " + usageMessage); - return false; - } - if (true) { executeSpigotTimings(sender, args); return true; } // Spigot - if (!sender.getServer().getPluginManager().useTimings()) { - sender.sendMessage("Please enable timings by setting \"settings.plugin-profiling\" to true in bukkit.yml"); - return true; - } - - boolean separate = "separate".equalsIgnoreCase(args[0]); - if ("reset".equalsIgnoreCase(args[0])) { - for (HandlerList handlerList : HandlerList.getHandlerLists()) { - for (RegisteredListener listener : handlerList.getRegisteredListeners()) { - if (listener instanceof TimedRegisteredListener) { - ((TimedRegisteredListener)listener).reset(); - } - } - } - sender.sendMessage("Timings reset"); - } else if ("merged".equalsIgnoreCase(args[0]) || separate) { - - int index = 0; - int pluginIdx = 0; - File timingFolder = new File("timings"); - timingFolder.mkdirs(); - File timings = new File(timingFolder, "timings.txt"); - File names = null; - while (timings.exists()) timings = new File(timingFolder, "timings" + (++index) + ".txt"); - PrintStream fileTimings = null; - PrintStream fileNames = null; - try { - fileTimings = new PrintStream(timings); - if (separate) { - names = new File(timingFolder, "names" + index + ".txt"); - fileNames = new PrintStream(names); - } - for (Plugin plugin : Bukkit.getPluginManager().getPlugins()) { - pluginIdx++; - long totalTime = 0; - if (separate) { - fileNames.println(pluginIdx + " " + plugin.getDescription().getFullName()); - fileTimings.println("Plugin " + pluginIdx); - } - else fileTimings.println(plugin.getDescription().getFullName()); - for (RegisteredListener listener : HandlerList.getRegisteredListeners(plugin)) { - if (listener instanceof TimedRegisteredListener) { - TimedRegisteredListener trl = (TimedRegisteredListener) listener; - long time = trl.getTotalTime(); - int count = trl.getCount(); - if (count == 0) continue; - long avg = time / count; - totalTime += time; - Class eventClass = trl.getEventClass(); - if (count > 0 && eventClass != null) { - fileTimings.println(" " + eventClass.getSimpleName() + (trl.hasMultiple() ? " (and sub-classes)" : "") + " Time: " + time + " Count: " + count + " Avg: " + avg); - } - } - } - fileTimings.println(" Total time " + totalTime + " (" + totalTime / 1000000000 + "s)"); - } - sender.sendMessage("Timings written to " + timings.getPath()); - if (separate) sender.sendMessage("Names written to " + names.getPath()); - } catch (IOException e) { - } finally { - if (fileTimings != null) { - fileTimings.close(); - } - if (fileNames != null) { - fileNames.close(); - } - } - } else { - sender.sendMessage(ChatColor.RED + "Usage: " + usageMessage); - return false; - } - return true; - } - - @Override - public List tabComplete(CommandSender sender, String alias, String[] args) { - Validate.notNull(sender, "Sender cannot be null"); - Validate.notNull(args, "Arguments cannot be null"); - Validate.notNull(alias, "Alias cannot be null"); - - if (args.length == 1) { - return StringUtil.copyPartialMatches(args[0], TIMINGS_SUBCOMMANDS, new ArrayList(TIMINGS_SUBCOMMANDS.size())); - } - return ImmutableList.of(); - } - - // Spigot start - private static class PasteThread extends Thread - { - - private final CommandSender sender; - private final ByteArrayOutputStream bout; - - public PasteThread(CommandSender sender, ByteArrayOutputStream bout) - { - super( "Timings paste thread" ); - this.sender = sender; - this.bout = bout; - } - - @Override - public synchronized void start() { - if (sender instanceof RemoteConsoleCommandSender) { - run(); - } else { - super.start(); - } - } - - @Override - public void run() - { - try - { - HttpURLConnection con = (HttpURLConnection) new URL( "https://timings.spigotmc.org/paste" ).openConnection(); - con.setDoOutput( true ); - con.setRequestMethod( "POST" ); - con.setInstanceFollowRedirects( false ); - - OutputStream out = con.getOutputStream(); - out.write( bout.toByteArray() ); - out.close(); - - com.google.gson.JsonObject location = new com.google.gson.Gson().fromJson(new java.io.InputStreamReader(con.getInputStream()), com.google.gson.JsonObject.class); - con.getInputStream().close(); - - String pasteID = location.get( "key" ).getAsString(); - sender.sendMessage( ChatColor.GREEN + "Timings results can be viewed at https://www.spigotmc.org/go/timings?url=" + pasteID ); - } catch ( IOException ex ) - { - sender.sendMessage( ChatColor.RED + "Error pasting timings, check your console for more information" ); - Bukkit.getServer().getLogger().log( Level.WARNING, "Could not paste timings", ex ); - } - } - } - // Spigot end -} diff --git a/src/main/java/org/bukkit/entity/Player.java b/src/main/java/org/bukkit/entity/Player.java index 5a2dc32..5fbad9f 100644 --- a/src/main/java/org/bukkit/entity/Player.java +++ b/src/main/java/org/bukkit/entity/Player.java @@ -1443,6 +1443,11 @@ public interface Player extends HumanEntity, Conversable, CommandSender, Offline public void sendMessage(net.md_5.bungee.api.ChatMessageType position, net.md_5.bungee.api.chat.BaseComponent... components) { throw new UnsupportedOperationException("Not supported yet."); } + + public int getPing() + { + throw new UnsupportedOperationException( "Not supported yet." ); + } } Spigot spigot(); diff --git a/src/main/java/org/bukkit/plugin/SimplePluginManager.java b/src/main/java/org/bukkit/plugin/SimplePluginManager.java index 1056186..4ee123e 100644 --- a/src/main/java/org/bukkit/plugin/SimplePluginManager.java +++ b/src/main/java/org/bukkit/plugin/SimplePluginManager.java @@ -295,7 +295,6 @@ public final class SimplePluginManager implements PluginManager { } } - org.bukkit.command.defaults.TimingsCommand.timingStart = System.nanoTime(); // Spigot return result.toArray(new Plugin[result.size()]); } @@ -332,7 +331,7 @@ public final class SimplePluginManager implements PluginManager { if (result != null) { plugins.add(result); - lookupNames.put(result.getDescription().getName(), result); + lookupNames.put(result.getDescription().getName().toLowerCase(java.util.Locale.ENGLISH), result); // Spigot } return result; @@ -358,7 +357,7 @@ public final class SimplePluginManager implements PluginManager { * @return Plugin if it exists, otherwise null */ public synchronized Plugin getPlugin(String name) { - return lookupNames.get(name.replace(' ', '_')); + return lookupNames.get(name.replace(' ', '_').toLowerCase(java.util.Locale.ENGLISH)); // Spigot } public synchronized Plugin[] getPlugins() { @@ -556,7 +555,8 @@ public final class SimplePluginManager implements PluginManager { throw new IllegalPluginAccessException("Plugin attempted to register " + event + " while not enabled"); } - if (useTimings) { + executor = new co.aikar.timings.TimedEventExecutor(executor, plugin, null, event); // Spigot + if (false) { // Spigot - RL handles useTimings check now getEventListeners(event).register(new TimedRegisteredListener(listener, executor, priority, plugin, ignoreCancelled)); } else { getEventListeners(event).register(new RegisteredListener(listener, executor, priority, plugin, ignoreCancelled)); @@ -717,7 +717,7 @@ public final class SimplePluginManager implements PluginManager { } public boolean useTimings() { - return useTimings; + return co.aikar.timings.Timings.isTimingsEnabled(); // Spigot } /** @@ -726,6 +726,6 @@ public final class SimplePluginManager implements PluginManager { * @param use True if per event timing code should be used */ public void useTimings(boolean use) { - useTimings = use; + co.aikar.timings.Timings.setTimingsEnabled(use); // Spigot } } diff --git a/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java b/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java index 80c6a72..759c461 100644 --- a/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java +++ b/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java @@ -40,7 +40,6 @@ import org.bukkit.plugin.PluginLoader; import org.bukkit.plugin.RegisteredListener; import org.bukkit.plugin.TimedRegisteredListener; import org.bukkit.plugin.UnknownDependencyException; -import org.spigotmc.CustomTimingsHandler; // Spigot import org.yaml.snakeyaml.error.YAMLException; /** @@ -51,7 +50,6 @@ public final class JavaPluginLoader implements PluginLoader { private final Pattern[] fileFilters = new Pattern[] { Pattern.compile("\\.jar$"), }; private final Map> classes = new java.util.concurrent.ConcurrentHashMap>(); // Spigot private final List loaders = new CopyOnWriteArrayList(); - public static final CustomTimingsHandler pluginParentTimer = new CustomTimingsHandler("** Plugins"); // Spigot /** * This class was not meant to be constructed explicitly @@ -289,26 +287,20 @@ public final class JavaPluginLoader implements PluginLoader { } } - final CustomTimingsHandler timings = new CustomTimingsHandler("Plugin: " + plugin.getDescription().getFullName() + " Event: " + listener.getClass().getName() + "::" + method.getName()+"("+eventClass.getSimpleName()+")", pluginParentTimer); // Spigot - EventExecutor executor = new EventExecutor() { + EventExecutor executor = new co.aikar.timings.TimedEventExecutor(new EventExecutor() { // Spigot public void execute(Listener listener, Event event) throws EventException { try { if (!eventClass.isAssignableFrom(event.getClass())) { return; } - // Spigot start - boolean isAsync = event.isAsynchronous(); - if (!isAsync) timings.startTiming(); method.invoke(listener, event); - if (!isAsync) timings.stopTiming(); - // Spigot end } catch (InvocationTargetException ex) { throw new EventException(ex.getCause()); } catch (Throwable t) { throw new EventException(t); } } - }; + }, plugin, method, eventClass); // Spigot if (false) { // Spigot - RL handles useTimings check now eventSet.add(new TimedRegisteredListener(listener, executor, eh.priority(), plugin, eh.ignoreCancelled())); } else { diff --git a/src/main/java/org/bukkit/plugin/java/PluginClassLoader.java b/src/main/java/org/bukkit/plugin/java/PluginClassLoader.java index 4cffa13..b2cbf9e 100644 --- a/src/main/java/org/bukkit/plugin/java/PluginClassLoader.java +++ b/src/main/java/org/bukkit/plugin/java/PluginClassLoader.java @@ -15,7 +15,8 @@ import org.bukkit.plugin.PluginDescriptionFile; /** * A ClassLoader for plugins, to allow shared classes across multiple plugins */ -final class PluginClassLoader extends URLClassLoader { +public final class PluginClassLoader extends URLClassLoader { // Spigot + public JavaPlugin getPlugin() { return plugin; } // Spigot private final JavaPluginLoader loader; private final Map> classes = new java.util.concurrent.ConcurrentHashMap>(); // Spigot private final PluginDescriptionFile description; diff --git a/src/main/java/org/bukkit/util/CachedServerIcon.java b/src/main/java/org/bukkit/util/CachedServerIcon.java index 5ca863b..0480470 100644 --- a/src/main/java/org/bukkit/util/CachedServerIcon.java +++ b/src/main/java/org/bukkit/util/CachedServerIcon.java @@ -12,4 +12,6 @@ import org.bukkit.event.server.ServerListPingEvent; * @see Server#loadServerIcon(java.io.File) * @see ServerListPingEvent#setServerIcon(CachedServerIcon) */ -public interface CachedServerIcon {} +public interface CachedServerIcon { + public String getData(); // Spigot +} diff --git a/src/main/java/org/spigotmc/CustomTimingsHandler.java b/src/main/java/org/spigotmc/CustomTimingsHandler.java index 8d98297..7e89b97 100644 --- a/src/main/java/org/spigotmc/CustomTimingsHandler.java +++ b/src/main/java/org/spigotmc/CustomTimingsHandler.java @@ -1,165 +1,76 @@ +/* + * 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 org.bukkit.command.defaults.TimingsCommand; -import org.bukkit.event.HandlerList; +import org.bukkit.Bukkit; +import org.bukkit.plugin.AuthorNagException; import org.bukkit.plugin.Plugin; -import org.bukkit.plugin.RegisteredListener; -import org.bukkit.plugin.TimedRegisteredListener; -import java.io.PrintStream; -import java.util.Collection; -import java.util.HashSet; -import java.util.List; -import java.util.Queue; -import java.util.concurrent.ConcurrentLinkedQueue; +import co.aikar.timings.NullTimingHandler; +import co.aikar.timings.Timing; +import co.aikar.timings.Timings; +import co.aikar.timings.TimingsManager; +import sun.reflect.Reflection; -import org.bukkit.Bukkit; -import org.bukkit.World; +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 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(String name) { + Timing timing; - public CustomTimingsHandler(String name) - { - this( name, null ); - } + Plugin plugin = null; + try { + plugin = TimingsManager.getPluginByClassloader(Reflection.getCallerClass(2)); + } catch (Exception ignored) {} - public CustomTimingsHandler(String name, CustomTimingsHandler parent) - { - this.name = name; - this.parent = parent; - HANDLERS.add( this ); - } - - /** - * Prints the timings and extra data to the given stream. - * - * @param printStream - */ - public static void printTimings(PrintStream printStream) - { - printStream.println( "Minecraft" ); - for ( CustomTimingsHandler timings : HANDLERS ) - { - long time = timings.totalTime; - long count = timings.count; - if ( count == 0 ) - { - continue; + 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.getMethod("getHandler", String.class, String.class, Timing.class, boolean.class); + timing = (Timing) ofSafe.invoke("Minecraft", "(Deprecated API) " + name, null, true); + } catch (Exception e) { + Bukkit.getLogger().log(Level.SEVERE, "This handler could not be registered"); + timing = Timings.NULL_HANDLER; } - 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 ); + handler = timing; } - /** - * Resets all timings. - */ - public static void reload() - { - if ( Bukkit.getPluginManager().useTimings() ) - { - for ( CustomTimingsHandler timings : HANDLERS ) - { - timings.reset(); - } - } - TimingsCommand.timingStart = System.nanoTime(); - } + public void startTiming() { handler.startTiming(); } + public void stopTiming() { handler.stopTiming(); } - /** - * Ticked every tick by CraftBukkit to count the number of times a timer - * caused TPS loss. - */ - public static void tick() - { - if ( Bukkit.getPluginManager().useTimings() ) - { - for ( CustomTimingsHandler timings : HANDLERS ) - { - if ( timings.curTickTotal > 50000000 ) - { - timings.violations += Math.ceil( timings.curTickTotal / 50000000 ); - } - timings.curTickTotal = 0; - timings.timingDepth = 0; // incase reset messes this up - } - } - } - - /** - * Starts timing to track a section of code. - */ - public void startTiming() - { - // If second condtion fails we are already timing - if ( Bukkit.getPluginManager().useTimings() && ++timingDepth == 1 ) - { - start = System.nanoTime(); - if ( parent != null && ++parent.timingDepth == 1 ) - { - parent.start = start; - } - } - } - - /** - * Stops timing a section of code. - */ - public void stopTiming() - { - if ( Bukkit.getPluginManager().useTimings() ) - { - if ( --timingDepth != 0 || start == 0 ) - { - return; - } - long diff = System.nanoTime() - start; - totalTime += diff; - curTickTotal += diff; - count++; - start = 0; - if ( parent != null ) - { - parent.stopTiming(); - } - } - } - - /** - * Reset this timer, setting all values to zero. - */ - public void reset() - { - count = 0; - violations = 0; - curTickTotal = 0; - totalTime = 0; - start = 0; - timingDepth = 0; - } } -- 2.9.3