From 2913f79fb789598971e82b36b116972502375d43 Mon Sep 17 00:00:00 2001 From: Aikar <aikar@aikar.co> Date: Mon, 29 Feb 2016 18:48:17 -0600 Subject: [PATCH] Timings v2 diff --git a/pom.xml b/pom.xml index 2e8b318..3be5e5b 100644 --- a/pom.xml +++ b/pom.xml @@ -1,4 +1,3 @@ - <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> @@ -48,6 +47,13 @@ <dependencies> <dependency> + <groupId>net.sf.trove4j</groupId> + <artifactId>trove4j</artifactId> + <version>3.0.3</version> + <!-- Trove Provided by CraftBukkit --> + <scope>provided</scope> + </dependency> + <dependency> <groupId>commons-lang</groupId> <artifactId>commons-lang</artifactId> <version>2.6</version> 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..cb4e7ba --- /dev/null +++ b/src/main/java/co/aikar/timings/FullServerTickHandler.java @@ -0,0 +1,79 @@ +package co.aikar.timings; + +import static co.aikar.timings.TimingsManager.*; + +public class FullServerTickHandler extends TimingHandler { + 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 void startTiming() { + if (TimingsManager.needsFullReset) { + TimingsManager.resetTimings(); + } else if (TimingsManager.needsRecheckEnabled) { + TimingsManager.recheckEnabled(); + } + super.startTiming(); + } + + @Override + public void stopTiming() { + super.stopTiming(); + if (!enabled) { + 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.curTickCount--; + minuteData.curTickTotal = record.curTickTotal; + minuteData.curTickCount = 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.curTickTotal > 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..c73b617 --- /dev/null +++ b/src/main/java/co/aikar/timings/NullTimingHandler.java @@ -0,0 +1,61 @@ +/* + * This file is licensed under the MIT License (MIT). + * + * Copyright (c) 2014 Daniel Ennis <http://aikar.co> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package co.aikar.timings; + +public final class NullTimingHandler implements Timing { + @Override + public void startTiming() { + + } + + @Override + public void stopTiming() { + + } + + @Override + public void startTimingIfSync() { + + } + + @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..e3da034 --- /dev/null +++ b/src/main/java/co/aikar/timings/TimedEventExecutor.java @@ -0,0 +1,81 @@ +/* + * This file is licensed under the MIT License (MIT). + * + * Copyright (c) 2014 Daniel Ennis <http://aikar.co> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package co.aikar.timings; + +import org.bukkit.Bukkit; +import org.bukkit.event.Event; +import org.bukkit.event.EventException; +import org.bukkit.event.Listener; +import org.bukkit.plugin.EventExecutor; +import org.bukkit.plugin.Plugin; + +import java.lang.reflect.Method; + +public class TimedEventExecutor implements EventExecutor { + + private final EventExecutor executor; + private final Timing timings; + + /** + * Wraps an event executor and associates a timing handler to it. + * + * @param executor + * @param plugin + * @param method + * @param eventClass + */ + public TimedEventExecutor(EventExecutor executor, Plugin plugin, Method method, Class<? extends Event> eventClass) { + this.executor = executor; + String id; + + if (method == null) { + if (executor.getClass().getEnclosingClass() != null) { // Oh Skript, how we love you + method = executor.getClass().getEnclosingMethod(); + } + } + + if (method != null) { + id = method.getDeclaringClass().getName(); + } else { + id = executor.getClass().getName(); + } + + + final String eventName = eventClass.getSimpleName(); + boolean verbose = "BlockPhysicsEvent".equals(eventName) || "Drain".equals(eventName) || "Fill".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..4d990b1 --- /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 <http://aikar.co> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package co.aikar.timings; + +/** + * Provides an ability to time sections of code within the Minecraft Server + */ +public interface Timing extends AutoCloseable { + /** + * Starts timing the execution until {@link #stopTiming()} is called. + */ + public void startTiming(); + + /** + * <p>Stops timing and records the data. Propagates the data up to group handlers.</p> + * + * Will automatically be called when this Timing is used with try-with-resources + */ + public void stopTiming(); + + /** + * Starts timing the execution until {@link #stopTiming()} is called. + * + * But only if we are on the primary thread. + */ + public void startTimingIfSync(); + + /** + * <p>Stops timing and records the data. Propagates the data up to group handlers.</p> + * + * <p>Will automatically be called when this Timing is used with try-with-resources</p> + * + * But only if we are on the primary thread. + */ + public void stopTimingIfSync(); + + /** + * Stops timing and disregards current timing data. + */ + public 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..b62e428 --- /dev/null +++ b/src/main/java/co/aikar/timings/TimingData.java @@ -0,0 +1,105 @@ +/* + * This file is licensed under the MIT License (MIT). + * + * Copyright (c) 2014 Daniel Ennis <http://aikar.co> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package co.aikar.timings; + +import com.google.common.base.Function; + +import java.util.List; + +import static co.aikar.util.JSONUtil.toArray; + +/** + * <p>Lightweight object for tracking timing data</p> + * + * This is broken out to reduce memory usage + */ +class TimingData { + static Function<Integer, TimingData> LOADER = new Function<Integer, TimingData>() { + @Override + public TimingData apply(Integer input) { + return new TimingData(input); + } + }; + int id; + int count = 0; + int lagCount = 0; + long totalTime = 0; + long lagTotalTime = 0; + + int curTickCount = 0; + int curTickTotal = 0; + + TimingData(int id) { + this.id = id; + } + + 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); + } + + public List export() { + List list = toArray( + id, + count, + totalTime); + if (lagCount > 0) { + list.add(lagCount); + list.add(lagTotalTime); + } + return list; + } +} 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..4918a17 --- /dev/null +++ b/src/main/java/co/aikar/timings/TimingHandler.java @@ -0,0 +1,193 @@ +/* + * This file is licensed under the MIT License (MIT). + * + * Copyright (c) 2014 Daniel Ennis <http://aikar.co> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package co.aikar.timings; + +import gnu.trove.map.hash.TIntObjectHashMap; +import org.bukkit.Bukkit; +import co.aikar.util.LoadingIntMap; +import co.aikar.util.LoadingMap; +import co.aikar.util.MRUMapCache; + +import java.util.Map; +import java.util.logging.Level; + +class TimingHandler implements Timing { + + private static int idPool = 1; + final int id = idPool++; + + final String name; + final boolean verbose; + + final TIntObjectHashMap<TimingData> children = new LoadingIntMap<TimingData>(TimingData.LOADER); + + final TimingData record; + final TimingHandler groupHandler; + + long start = 0; + int timingDepth = 0; + boolean added; + boolean timed; + boolean enabled; + 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.curTickCount == 0) { + timingDepth = 0; + start = 0; + return; + } + + record.processTick(violated); + for (TimingData handler : children.valueCollection()) { + handler.processTick(violated); + } + } + + @Override + public void startTimingIfSync() { + if (Bukkit.isPrimaryThread()) { + startTiming(); + } + } + + @Override + public void stopTimingIfSync() { + if (Bukkit.isPrimaryThread()) { + stopTiming(); + } + } + + public void startTiming() { + if (enabled && ++timingDepth == 1) { + start = System.nanoTime(); + parent = TimingsManager.CURRENT; + TimingsManager.CURRENT = 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; + } +} 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..7a04770 --- /dev/null +++ b/src/main/java/co/aikar/timings/TimingHistory.java @@ -0,0 +1,276 @@ +/* + * This file is licensed under the MIT License (MIT). + * + * Copyright (c) 2014 Daniel Ennis <http://aikar.co> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package co.aikar.timings; + +import com.google.common.base.Function; +import 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"}) +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; + static int worldIdPool = 1; + static Map<String, Integer> worldMap = LoadingMap.newHashMap(new Function<String, Integer>() { + @Override + public Integer apply(String input) { + return worldIdPool++; + } + }); + final long endTime; + final long startTime; + final long totalTicks; + final long totalTime; // Represents all time spent running the server this history + final MinuteReport[] minuteReports; + + final TimingHistoryEntry[] entries; + final Set<Material> tileEntityTypeSet = Sets.newHashSet(); + final Set<EntityType> entityTypeSet = Sets.newHashSet(); + final Map<Object, Object> worlds; + + TimingHistory() { + this.endTime = System.currentTimeMillis() / 1000; + this.startTime = TimingsManager.historyStart / 1000; + if (timedTicks % 1200 != 0 || MINUTE_REPORTS.isEmpty()) { + this.minuteReports = MINUTE_REPORTS.toArray(new MinuteReport[MINUTE_REPORTS.size() + 1]); + this.minuteReports[this.minuteReports.length - 1] = new MinuteReport(); + } else { + this.minuteReports = MINUTE_REPORTS.toArray(new MinuteReport[MINUTE_REPORTS.size()]); + } + long ticks = 0; + for (MinuteReport mp : this.minuteReports) { + ticks += mp.ticksRecord.timed; + } + this.totalTicks = ticks; + this.totalTime = FULL_SERVER_TICK.record.totalTime; + this.entries = new TimingHistoryEntry[TimingsManager.HANDLERS.size()]; + + int i = 0; + for (TimingHandler handler : TimingsManager.HANDLERS) { + entries[i++] = new TimingHistoryEntry(handler); + } + + final Map<EntityType, Counter> entityCounts = MRUMapCache.of(LoadingMap.of( + new EnumMap<EntityType, Counter>(EntityType.class), Counter.LOADER + )); + final Map<Material, Counter> tileEntityCounts = MRUMapCache.of(LoadingMap.of( + new EnumMap<Material, Counter>(Material.class), Counter.LOADER + )); + // Information about all loaded chunks/entities + this.worlds = toObjectMapper(Bukkit.getWorlds(), new Function<World, JSONPair>() { + @Override + public JSONPair apply(World world) { + return pair( + worldMap.get(world.getName()), + toArrayMapper(world.getLoadedChunks(), new Function<Chunk, Object>() { + @Override + public Object apply(Chunk chunk) { + entityCounts.clear(); + tileEntityCounts.clear(); + + for (Entity entity : chunk.getEntities()) { + entityCounts.get(entity.getType()).increment(); + } + + for (BlockState tileEntity : chunk.getTileEntities()) { + tileEntityCounts.get(tileEntity.getBlock().getType()).increment(); + } + + if (tileEntityCounts.isEmpty() && entityCounts.isEmpty()) { + return null; + } + return toArray( + chunk.getX(), + chunk.getZ(), + toObjectMapper(entityCounts.entrySet(), + new Function<Map.Entry<EntityType, Counter>, JSONPair>() { + @Override + public JSONPair apply(Map.Entry<EntityType, Counter> entry) { + entityTypeSet.add(entry.getKey()); + return pair( + String.valueOf(entry.getKey().getTypeId()), + entry.getValue().count() + ); + } + } + ), + toObjectMapper(tileEntityCounts.entrySet(), + new Function<Map.Entry<Material, Counter>, JSONPair>() { + @Override + public JSONPair apply(Map.Entry<Material, Counter> entry) { + tileEntityTypeSet.add(entry.getKey()); + return pair( + String.valueOf(entry.getKey().getId()), + entry.getValue().count() + ); + } + } + ) + ); + } + }) + ); + } + }); + } + + public static void resetTicks(boolean fullReset) { + if (fullReset) { + // Non full is simply for 1 minute reports + timedTicks = 0; + } + lastMinuteTime = System.nanoTime(); + playerTicks = 0; + tileEntityTicks = 0; + entityTicks = 0; + activatedEntityTicks = 0; + } + + Object export() { + return createObject( + pair("s", startTime), + pair("e", endTime), + pair("tk", totalTicks), + pair("tm", totalTime), + pair("w", worlds), + pair("h", toArrayMapper(entries, new Function<TimingHistoryEntry, Object>() { + @Override + public Object apply(TimingHistoryEntry entry) { + TimingData record = entry.data; + if (record.count == 0) { + return null; + } + return entry.export(); + } + })), + pair("mp", toArrayMapper(minuteReports, new Function<MinuteReport, Object>() { + @Override + public Object apply(MinuteReport input) { + return input.export(); + } + })) + ); + } + + static class MinuteReport { + final long time = System.currentTimeMillis() / 1000; + + final TicksRecord ticksRecord = new TicksRecord(); + final PingRecord pingRecord = new PingRecord(); + final TimingData fst = TimingsManager.FULL_SERVER_TICK.minuteData.clone(); + final double tps = 1E9 / ( System.nanoTime() - lastMinuteTime ) * ticksRecord.timed; + final double usedMemory = TimingsManager.FULL_SERVER_TICK.avgUsedMemory; + final double freeMemory = TimingsManager.FULL_SERVER_TICK.avgFreeMemory; + final double loadAvg = ManagementFactory.getOperatingSystemMXBean().getSystemLoadAverage(); + + public 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 + ); + } + } + + 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; + } + + } + + static class PingRecord { + final double avg; + + PingRecord() { + final Collection<? extends Player> onlinePlayers = Bukkit.getOnlinePlayers(); + int totalPing = 0; + for (Player player : onlinePlayers) { + totalPing += player.spigot().getPing(); + } + avg = onlinePlayers.isEmpty() ? 0 : totalPing / onlinePlayers.size(); + } + } + + static class Counter { + int count = 0; + @SuppressWarnings({"rawtypes", "SuppressionAnnotation"}) + static Function LOADER = new LoadingMap.Feeder<Counter>() { + @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..eac4e21 --- /dev/null +++ b/src/main/java/co/aikar/timings/TimingHistoryEntry.java @@ -0,0 +1,59 @@ +/* + * This file is licensed under the MIT License (MIT). + * + * Copyright (c) 2014 Daniel Ennis <http://aikar.co> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package co.aikar.timings; + +import com.google.common.base.Function; + +import java.util.List; + +import static co.aikar.util.JSONUtil.toArrayMapper; + +class TimingHistoryEntry { + final TimingData data; + final TimingData[] children; + + TimingHistoryEntry(TimingHandler handler) { + this.data = handler.record.clone(); + children = new TimingData[handler.children.size()]; + int i = 0; + for (TimingData child : handler.children.valueCollection()) { + children[i++] = child.clone(); + } + } + + List export() { + List result = data.export(); + if (children.length > 0) { + result.add( + toArrayMapper(children, new Function<TimingData, Object>() { + @Override + public Object apply(TimingData child) { + return child.export(); + } + }) + ); + } + return result; + } +} diff --git a/src/main/java/co/aikar/timings/TimingIdentifier.java b/src/main/java/co/aikar/timings/TimingIdentifier.java new file mode 100644 index 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 <http://aikar.co> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package co.aikar.timings; + +import com.google.common.base.Function; +import co.aikar.util.LoadingMap; +import co.aikar.util.MRUMapCache; + +import java.util.ArrayDeque; +import java.util.Map; + +/** + * <p>Used as a basis for fast HashMap key comparisons for the Timing Map.</p> + * + * This class uses interned strings giving us the ability to do an identity check instead of equals() on the strings + */ +final class TimingIdentifier { + /** + * Holds all groups. Autoloads on request for a group by name. + */ + static final Map<String, TimingGroup> GROUP_MAP = MRUMapCache.of( + LoadingMap.newIdentityHashMap(new Function<String, TimingGroup>() { + @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<TimingHandler> handlers = new ArrayDeque<TimingHandler>(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..0f7be03 --- /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 <http://aikar.co> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package co.aikar.timings; + +import com.google.common.base.Preconditions; +import com.google.common.collect.EvictingQueue; +import org.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); + } + + /** + * <p>Returns a handler that has a groupHandler timer handler. Parent timers should not have their + * start/stop methods called directly, as the children will call it for you.</p> + * + * Parent Timers are used to group multiple subsections together and get a summary of them combined + * Parent Handler can not be changed after first call + * + * @param plugin Plugin to own the Timing + * @param name Name of Timing + * @param groupHandler Parent handler to mirror .start/stop calls to + * @return Timing Handler + */ + public static Timing of(Plugin plugin, String name, Timing groupHandler) { + Preconditions.checkNotNull(plugin, "Plugin can not be null"); + return TimingsManager.getHandler(plugin.getName(), name, groupHandler, 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; + } + + /** + * <p>Sets whether or not the Spigot Timings system should be enabled</p> + * + * Calling this will reset timing data. + * + * @param enabled Should timings be reported + */ + public static void setTimingsEnabled(boolean enabled) { + timingsEnabled = enabled; + reset(); + } + + /** + * <p>Sets whether or not the Timings should monitor at Verbose level.</p> + * + * <p>When Verbose is disabled, high-frequency timings will not be available.</p> + * + * @return Enabled or not + */ + public static boolean isVerboseTimingsEnabled() { + return timingsEnabled; + } + + /** + * Sets whether or not the Timings should monitor at Verbose level. + * <p/> + * When Verbose is disabled, high-frequency timings will not be available. + * Calling this will reset timing data. + * + * @param enabled Should high-frequency timings be reported + */ + public static void setVerboseTimingsEnabled(boolean enabled) { + verboseEnabled = enabled; + TimingsManager.needsRecheckEnabled = true; + } + + /** + * <p>Gets the interval between Timing History report generation.</p> + * + * Defaults to 5 minutes (6000 ticks) + * + * @return Interval in ticks + */ + public static int getHistoryInterval() { + return historyInterval; + } + + /** + * <p>Sets the interval between Timing History report generations.</p> + * + * <p>Defaults to 5 minutes (6000 ticks)</p> + * + * This will recheck your history length, so lowering this value will lower your + * history length if you need more than 60 history windows. + * + * @param interval Interval in ticks + */ + public static void setHistoryInterval(int interval) { + historyInterval = Math.max(20*60, interval); + // Recheck the history length with the new Interval + if (historyLength != -1) { + setHistoryLength(historyLength); + } + } + + /** + * Gets how long in ticks Timings history is kept for the server. + * + * Defaults to 1 hour (72000 ticks) + * + * @return Duration in Ticks + */ + public static int getHistoryLength() { + return historyLength; + } + + /** + * Sets how long Timing History reports are kept for the server. + * + * Defaults to 1 hours(72000 ticks) + * + * This value is capped at a maximum of getHistoryInterval() * MAX_HISTORY_FRAMES (12) + * + * Will not reset Timing Data but may truncate old history if the new length is less than old length. + * + * @param length Duration in ticks + */ + public static void setHistoryLength(int length) { + // Cap at 12 History Frames, 1 hour at 5 minute frames. + int maxLength = historyInterval * MAX_HISTORY_FRAMES; + // For special cases of servers with special permission to bypass the max. + // This max helps keep data file sizes reasonable for processing on Aikar's Timing parser side. + // Setting this will not help you bypass the max unless Aikar has added an exception on the API side. + if (System.getProperty("timings.bypassMax") != null) { + maxLength = Integer.MAX_VALUE; + } + historyLength = Math.max(Math.min(maxLength, length), historyInterval); + Queue<TimingHistory> oldQueue = TimingsManager.HISTORY; + int frames = (getHistoryLength() / getHistoryInterval()); + if (length > maxLength) { + Bukkit.getLogger().log(Level.WARNING, "Timings Length too high. Requested " + length + ", max is " + maxLength + ". To get longer history, you must increase your interval. Set Interval to " + Math.ceil(length / MAX_HISTORY_FRAMES) + " to achieve this length."); + } + TimingsManager.HISTORY = EvictingQueue.create(frames); + TimingsManager.HISTORY.addAll(oldQueue); + } + + /** + * Resets all Timing Data + */ + public static void reset() { + TimingsManager.reset(); + } + + /** + * Generates a report and sends it to the specified command sender. + * + * If sender is null, ConsoleCommandSender will be used. + * @param sender The sender to send to, or null to use the ConsoleCommandSender + */ + public static void generateReport(CommandSender sender) { + if (sender == null) { + sender = Bukkit.getConsoleSender(); + } + TimingsExport.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..3dba3aa --- /dev/null +++ b/src/main/java/co/aikar/timings/TimingsCommand.java @@ -0,0 +1,110 @@ +/* + * This file is licensed under the MIT License (MIT). + * + * Copyright (c) 2014 Daniel Ennis <http://aikar.co> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package co.aikar.timings; + +import com.google.common.collect.ImmutableList; +import org.apache.commons.lang.Validate; +import org.bukkit.ChatColor; +import org.bukkit.command.CommandSender; +import org.bukkit.command.defaults.BukkitCommand; +import org.bukkit.util.StringUtil; + +import java.util.ArrayList; +import java.util.List; + + +public class TimingsCommand extends BukkitCommand { + public static final List<String> TIMINGS_SUBCOMMANDS = ImmutableList.of("report", "reset", "on", "off", "paste", "verbon", "verboff"); + + public TimingsCommand(String name) { + super(name); + this.description = "Manages Spigot Timings data to see performance of the server."; + this.usageMessage = "/timings <reset|report|on|off|verbon|verboff>"; + this.setPermission("bukkit.command.timings"); + } + + @Override + public boolean execute(CommandSender sender, String currentAlias, String[] args) { + if (!testPermission(sender)) { + return true; + } + if (args.length < 1) { + sender.sendMessage(ChatColor.RED + "Usage: " + usageMessage); + return true; + } + final String arg = args[0]; + if ("on".equalsIgnoreCase(arg)) { + Timings.setTimingsEnabled(true); + sender.sendMessage("Enabled Timings & Reset"); + return true; + } else if ("off".equalsIgnoreCase(arg)) { + Timings.setTimingsEnabled(false); + sender.sendMessage("Disabled Timings"); + return true; + } + + if (!Timings.isTimingsEnabled()) { + sender.sendMessage("Please enable timings by typing /timings on"); + return true; + } + 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)) { + TimingsManager.reset(); + sender.sendMessage("Timings reset"); + } 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<String> tabComplete(CommandSender sender, String alias, String[] args) { + Validate.notNull(sender, "Sender cannot be null"); + Validate.notNull(args, "Arguments cannot be null"); + Validate.notNull(alias, "Alias cannot be null"); + + if (args.length == 1) { + return StringUtil.copyPartialMatches(args[0], TIMINGS_SUBCOMMANDS, + new ArrayList<String>(TIMINGS_SUBCOMMANDS.size())); + } + return ImmutableList.of(); + } +} diff --git a/src/main/java/co/aikar/timings/TimingsExport.java b/src/main/java/co/aikar/timings/TimingsExport.java new file mode 100644 index 0000000..fe19ea0 --- /dev/null +++ b/src/main/java/co/aikar/timings/TimingsExport.java @@ -0,0 +1,373 @@ +/* + * This file is licensed under the MIT License (MIT). + * + * Copyright (c) 2014 Daniel Ennis <http://aikar.co> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package co.aikar.timings; + +import com.google.common.base.Function; +import 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.OperatingSystemMXBean; +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; + + 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) { + 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<GarbageCollectorMXBean, JSONPair>() { + @Override + public JSONPair apply(GarbageCollectorMXBean input) { + return pair(input.getName(), toArray(input.getCollectionCount(), input.getCollectionTime())); + } + })) + ) + ); + + Set<Material> tileEntityTypeSet = Sets.newHashSet(); + Set<EntityType> entityTypeSet = Sets.newHashSet(); + + int size = HISTORY.size(); + TimingHistory[] history = new TimingHistory[size + 1]; + int i = 0; + for (TimingHistory timingHistory : HISTORY) { + tileEntityTypeSet.addAll(timingHistory.tileEntityTypeSet); + entityTypeSet.addAll(timingHistory.entityTypeSet); + history[i++] = timingHistory; + } + + history[i] = new TimingHistory(); // Current snapshot + tileEntityTypeSet.addAll(history[i].tileEntityTypeSet); + entityTypeSet.addAll(history[i].entityTypeSet); + + + Map handlers = createObject(); + for (TimingIdentifier.TimingGroup group : TimingIdentifier.GROUP_MAP.values()) { + for (TimingHandler id : group.handlers) { + if (!id.timed && !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<TimingIdentifier.TimingGroup, JSONPair>() { + @Override + public JSONPair apply(TimingIdentifier.TimingGroup group) { + return pair(group.id, group.name); + } + })), + pair("handlers", handlers), + pair("worlds", toObjectMapper(TimingHistory.worldMap.entrySet(), new Function<Map.Entry<String, Integer>, JSONPair>() { + @Override + public JSONPair apply(Map.Entry<String, Integer> input) { + return pair(input.getValue(), input.getKey()); + } + })), + pair("tileentity", + toObjectMapper(tileEntityTypeSet, new Function<Material, JSONPair>() { + @Override + public JSONPair apply(Material input) { + return pair(input.getId(), input.name()); + } + })), + pair("entity", + toObjectMapper(entityTypeSet, new Function<EntityType, JSONPair>() { + @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<Plugin, JSONPair>() { + @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("paperspigot", 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<Object> v = (Iterable<Object>) val; + return toArrayMapper(v, new Function<Object, Object>() { + @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<TimingHistory, Object>() { + @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); + con.setRequestProperty("User-Agent", "Spigot/" + Bukkit.getServerName() + "/" + InetAddress.getLocalHost().getHostName()); + 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..67c39df --- /dev/null +++ b/src/main/java/co/aikar/timings/TimingsManager.java @@ -0,0 +1,194 @@ +/* + * This file is licensed under the MIT License (MIT). + * + * Copyright (c) 2014 Daniel Ennis <http://aikar.co> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package co.aikar.timings; + +import com.google.common.base.Function; +import 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<TimingIdentifier, TimingHandler> TIMING_MAP = + Collections.synchronizedMap(LoadingMap.newHashMap( + new Function<TimingIdentifier, TimingHandler>() { + @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<String> hiddenConfigs = new ArrayList<String>(); + public static boolean privacy = false; + + static final Collection<TimingHandler> HANDLERS = new ArrayDeque<TimingHandler>(); + static final ArrayDeque<TimingHistory.MinuteReport> MINUTE_REPORTS = new ArrayDeque<TimingHistory.MinuteReport>(); + + static EvictingQueue<TimingHistory> HISTORY = EvictingQueue.create(12); + static TimingHandler CURRENT; + static long timingStart = 0; + static long historyStart = 0; + static boolean needsFullReset = false; + static boolean needsRecheckEnabled = false; + + private TimingsManager() {} + + /** + * Resets all timing data on the next tick + */ + static void reset() { + needsFullReset = true; + } + + /** + * Ticked every tick by CraftBukkit to count the number of times a timer + * caused TPS loss. + */ + static void tick() { + if (Timings.timingsEnabled) { + boolean violated = FULL_SERVER_TICK.isViolated(); + + for (TimingHandler handler : HANDLERS) { + if (handler.isSpecial()) { + // We manually call this + continue; + } + handler.processTick(violated); + } + + TimingHistory.playerTicks += Bukkit.getOnlinePlayers().size(); + TimingHistory.timedTicks++; + // Generate TPS/Ping/Tick reports every minute + } + } + static void stopServer() { + Timings.timingsEnabled = false; + recheckEnabled(); + } + static void recheckEnabled() { + synchronized (TIMING_MAP) { + for (TimingHandler timings : TIMING_MAP.values()) { + timings.checkEnabled(); + } + } + needsRecheckEnabled = false; + } + static void resetTimings() { + if (needsFullReset) { + // Full resets need to re-check every handlers enabled state + // Timing map can be modified from async so we must sync on it. + synchronized (TIMING_MAP) { + for (TimingHandler timings : TIMING_MAP.values()) { + timings.reset(true); + } + } + Bukkit.getLogger().log(Level.INFO, "Timings Reset"); + HISTORY.clear(); + needsFullReset = false; + needsRecheckEnabled = false; + timingStart = System.currentTimeMillis(); + } else { + // Soft resets only need to act on timings that have done something + // Handlers can only be modified on main thread. + for (TimingHandler timings : HANDLERS) { + timings.reset(false); + } + } + + HANDLERS.clear(); + MINUTE_REPORTS.clear(); + + TimingHistory.resetTicks(true); + historyStart = System.currentTimeMillis(); + } + + static TimingHandler getHandler(String group, String name, Timing parent, boolean protect) { + return TIMING_MAP.get(new TimingIdentifier(group, name, parent, protect)); + } + + + /** + * <p>Due to access restrictions, we need a helper method to get a Command TimingHandler with String group</p> + * + * Plugins should never call this + * + * @param pluginName Plugin this command is associated with + * @param command Command to get timings for + * @return TimingHandler + */ + public static Timing getCommandTiming(String pluginName, Command command) { + Plugin plugin = null; + final Server server = Bukkit.getServer(); + if (!("minecraft".equals(pluginName) || "bukkit".equals(pluginName) || "Spigot".equals(pluginName) || + server == null)) { + 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..e3b0ed8 --- /dev/null +++ b/src/main/java/co/aikar/timings/UnsafeTimingHandler.java @@ -0,0 +1,51 @@ +/* + * This file is licensed under the MIT License (MIT). + * + * Copyright (c) 2014 Daniel Ennis <http://aikar.co> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package co.aikar.timings; + +import org.bukkit.Bukkit; + +class UnsafeTimingHandler extends TimingHandler { + + UnsafeTimingHandler(TimingIdentifier id) { + super(id); + } + + private static void checkThread() { + if (!Bukkit.isPrimaryThread()) { + throw new IllegalStateException("Calling Timings from Async Operation"); + } + } + + @Override + public void startTiming() { + checkThread(); + 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 <E> + * @return + */ + public static <E> List toArrayMapper(E[] collection, Function<E, Object> mapper) { + return toArrayMapper(Lists.newArrayList(collection), mapper); + } + + public static <E> List toArrayMapper(Iterable<E> collection, Function<E, Object> mapper) { + List array = Lists.newArrayList(); + for (E e : collection) { + Object object = mapper.apply(e); + if (object != null) { + array.add(object); + } + } + return array; + } + + /** + * These help build a single JSON Object from a collection, using a mapper function + * @param collection + * @param mapper + * @param <E> + * @return + */ + public static <E> Map toObjectMapper(E[] collection, Function<E, JSONPair> mapper) { + return toObjectMapper(Lists.newArrayList(collection), mapper); + } + + public static <E> Map toObjectMapper(Iterable<E> collection, Function<E, JSONPair> mapper) { + Map object = Maps.newLinkedHashMap(); + for (E e : collection) { + JSONPair JSONPair = mapper.apply(e); + if (JSONPair != null) { + object.put(JSONPair.key, JSONPair.val); + } + } + return object; + } + + /** + * Simply stores a key and a value, used internally by many methods below. + */ + @SuppressWarnings("PublicInnerClass") + public static class JSONPair { + final String key; + final Object val; + + JSONPair(String key, Object val) { + this.key = key; + this.val = val; + } + } +} diff --git a/src/main/java/co/aikar/util/LoadingIntMap.java b/src/main/java/co/aikar/util/LoadingIntMap.java new file mode 100644 index 0000000..8d0f269 --- /dev/null +++ b/src/main/java/co/aikar/util/LoadingIntMap.java @@ -0,0 +1,63 @@ +/* + * 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 gnu.trove.map.hash.TIntObjectHashMap; + +/** + * Allows you to pass a Loader function that when a key is accessed that doesn't exist, + * automatically loads the entry into the map by calling the loader Function. + * + * .get() Will only return null if the Loader can return null. + * + * You may pass any backing Map to use. + * + * This class is not thread safe and should be wrapped with Collections.synchronizedMap on the OUTSIDE of the LoadingMap if needed. + * + * Do not wrap the backing map with Collections.synchronizedMap. + * + * @param <V> Value + */ +public class LoadingIntMap<V> extends TIntObjectHashMap<V> { + private final Function<Integer, V> loader; + + /** + * Initializes an auto loading map using specified loader and backing map + * @param loader The loader + */ + public LoadingIntMap(Function<Integer, V> loader) { + this.loader = loader; + } + + + @Override + public V get(int key) { + V res = super.get(key); + if (res == null) { + res = loader.apply(key); + if (res != null) { + put(key, res); + } + } + return res; + } + + /** + * Due to java stuff, you will need to cast it to (Function) for some cases + * @param <T> + */ + public abstract static class Feeder <T> implements Function<T, T> { + @Override + public T apply(Object input) { + return apply(); + } + + public abstract T apply(); + } +} diff --git a/src/main/java/co/aikar/util/LoadingMap.java b/src/main/java/co/aikar/util/LoadingMap.java new file mode 100644 index 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 <http://aikar.co> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package co.aikar.util; + + +import com.google.common.base.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 <K> Key + * @param <V> Value + */ +public class LoadingMap <K,V> extends AbstractMap<K, V> { + private final Map<K, V> backingMap; + private final Function<K, V> loader; + + /** + * Initializes an auto loading map using specified loader and backing map + * @param backingMap + * @param loader + */ + public LoadingMap(Map<K, V> backingMap, Function<K, V> loader) { + this.backingMap = backingMap; + this.loader = loader; + } + + /** + * Creates a new LoadingMap with the specified map and loader + * @param backingMap + * @param loader + * @param <K> + * @param <V> + * @return + */ + public static <K, V> Map<K, V> of(Map<K, V> backingMap, Function<K, V> loader) { + return new LoadingMap<K, V>(backingMap, loader); + } + + /** + * Creates a LoadingMap with an auto instantiating loader. + * + * Will auto construct class of of Value when not found + * + * Since this uses Reflection, It is more effecient to define your own static loader + * than using this helper, but if performance is not critical, this is easier. + * + * @param backingMap Actual map being used. + * @param keyClass Class used for the K generic + * @param valueClass Class used for the V generic + * @param <K> Key Type of the Map + * @param <V> Value Type of the Map + * @return Map that auto instantiates on .get() + */ + public static <K, V> Map<K, V> newAutoMap(Map<K, V> backingMap, final Class<? extends K> keyClass, + final Class<? extends V> valueClass) { + return new LoadingMap<K, V>(backingMap, new AutoInstantiatingLoader<K, V>(keyClass, valueClass)); + } + /** + * Creates a LoadingMap with an auto instantiating loader. + * + * Will auto construct class of of Value when not found + * + * Since this uses Reflection, It is more effecient to define your own static loader + * than using this helper, but if performance is not critical, this is easier. + * + * @param backingMap Actual map being used. + * @param valueClass Class used for the V generic + * @param <K> Key Type of the Map + * @param <V> Value Type of the Map + * @return Map that auto instantiates on .get() + */ + public static <K, V> Map<K, V> newAutoMap(Map<K, V> backingMap, + final Class<? extends V> valueClass) { + return newAutoMap(backingMap, null, valueClass); + } + + /** + * @see #newAutoMap + * + * new Auto initializing map using a HashMap. + * @param keyClass + * @param valueClass + * @param <K> + * @param <V> + * @return + */ + public static <K, V> Map<K, V> newHashAutoMap(final Class<? extends K> keyClass, final Class<? extends V> valueClass) { + return newAutoMap(new HashMap<K, V>(), keyClass, valueClass); + } + + /** + * @see #newAutoMap + * + * new Auto initializing map using a HashMap. + * @param valueClass + * @param <K> + * @param <V> + * @return + */ + public static <K, V> Map<K, V> newHashAutoMap(final Class<? extends V> valueClass) { + return newHashAutoMap(null, valueClass); + } + + /** + * @see #newAutoMap + * + * new Auto initializing map using a HashMap. + * + * @param keyClass + * @param valueClass + * @param initialCapacity + * @param loadFactor + * @param <K> + * @param <V> + * @return + */ + public static <K, V> Map<K, V> newHashAutoMap(final Class<? extends K> keyClass, final Class<? extends V> valueClass, int initialCapacity, float loadFactor) { + return newAutoMap(new HashMap<K, V>(initialCapacity, loadFactor), keyClass, valueClass); + } + + /** + * @see #newAutoMap + * + * new Auto initializing map using a HashMap. + * + * @param valueClass + * @param initialCapacity + * @param loadFactor + * @param <K> + * @param <V> + * @return + */ + public static <K, V> Map<K, V> newHashAutoMap(final Class<? extends V> valueClass, int initialCapacity, float loadFactor) { + return newHashAutoMap(null, valueClass, initialCapacity, loadFactor); + } + + /** + * Initializes an auto loading map using a HashMap + * @param loader + * @param <K> + * @param <V> + * @return + */ + public static <K, V> Map<K, V> newHashMap(Function<K, V> loader) { + return new LoadingMap<K, V>(new HashMap<K, V>(), loader); + } + + /** + * Initializes an auto loading map using a HashMap + * @param loader + * @param initialCapacity + * @param loadFactor + * @param <K> + * @param <V> + * @return + */ + public static <K, V> Map<K, V> newHashMap(Function<K, V> loader, int initialCapacity, float loadFactor) { + return new LoadingMap<K, V>(new HashMap<K, V>(initialCapacity, loadFactor), loader); + } + + /** + * Initializes an auto loading map using an Identity HashMap + * @param loader + * @param <K> + * @param <V> + * @return + */ + public static <K, V> Map<K, V> newIdentityHashMap(Function<K, V> loader) { + return new LoadingMap<K, V>(new IdentityHashMap<K, V>(), loader); + } + + /** + * Initializes an auto loading map using an Identity HashMap + * @param loader + * @param initialCapacity + * @param <K> + * @param <V> + * @return + */ + public static <K, V> Map<K, V> newIdentityHashMap(Function<K, V> loader, int initialCapacity) { + return new LoadingMap<K, V>(new IdentityHashMap<K, V>(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<? extends K, ? extends V> m) {backingMap.putAll(m);} + + @Override + public void clear() {backingMap.clear();} + + @Override + public Set<K> keySet() {return backingMap.keySet();} + + @Override + public Collection<V> values() {return backingMap.values();} + + @Override + public boolean equals(Object o) {return backingMap.equals(o);} + + @Override + public int hashCode() {return backingMap.hashCode();} + + @Override + public Set<Entry<K, V>> entrySet() { + return backingMap.entrySet(); + } + + public LoadingMap<K, V> clone() { + return new LoadingMap<K, V>(backingMap, loader); + } + + private static class AutoInstantiatingLoader<K, V> implements Function<K, V> { + final Constructor<? extends V> constructor; + private final Class<? extends V> valueClass; + + AutoInstantiatingLoader(Class<? extends K> keyClass, Class<? extends V> valueClass) { + try { + this.valueClass = valueClass; + if (keyClass != null) { + constructor = valueClass.getConstructor(keyClass); + } else { + constructor = null; + } + } catch (NoSuchMethodException e) { + throw new IllegalStateException( + valueClass.getName() + " does not have a constructor for " + (keyClass != null ? keyClass.getName() : null)); + } + } + + @Override + public V apply(K input) { + try { + return (constructor != null ? constructor.newInstance(input) : valueClass.newInstance()); + } catch (Exception e) { + throw new ExceptionInInitializerError(e); + } + } + + @Override + public int hashCode() { + return super.hashCode(); + } + + @Override + public boolean equals(Object object) { + return false; + } + } + + /** + * Due to java stuff, you will need to cast it to (Function) for some cases + * @param <T> + */ + public abstract static class Feeder <T> implements Function<T, T> { + @Override + public T apply(Object input) { + return apply(); + } + + public abstract T apply(); + } +} diff --git a/src/main/java/co/aikar/util/MRUMapCache.java b/src/main/java/co/aikar/util/MRUMapCache.java new file mode 100644 index 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 <http://aikar.co> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package co.aikar.util; + +import java.util.AbstractMap; +import java.util.Collection; +import java.util.Map; +import java.util.Set; + +/** + * Implements a Most Recently Used cache in front of a backing map, to quickly access the last accessed result. + * @param <K> + * @param <V> + */ +public class MRUMapCache<K, V> extends AbstractMap<K, V> { + final Map<K, V> backingMap; + Object cacheKey; + V cacheValue; + public MRUMapCache(final Map<K, V> backingMap) { + this.backingMap = backingMap; + } + + public int size() {return backingMap.size();} + + public boolean isEmpty() {return backingMap.isEmpty();} + + public boolean containsKey(Object key) { + return key != null && key.equals(cacheKey) || backingMap.containsKey(key); + } + + public boolean containsValue(Object value) { + return value != null && value == cacheValue || backingMap.containsValue(value); + } + + public V get(Object key) { + if (cacheKey != null && cacheKey.equals(key)) { + return cacheValue; + } + cacheKey = key; + return cacheValue = backingMap.get(key); + } + + public V put(K key, V value) { + cacheKey = key; + return cacheValue = backingMap.put(key, value); + } + + public V remove(Object key) { + if (key != null && key.equals(cacheKey)) { + cacheKey = null; + } + return backingMap.remove(key); + } + + public void putAll(Map<? extends K, ? extends V> m) {backingMap.putAll(m);} + + public void clear() { + cacheKey = null; + cacheValue = null; + backingMap.clear(); + } + + public Set<K> keySet() {return backingMap.keySet();} + + public Collection<V> values() {return backingMap.values();} + + public Set<Map.Entry<K, V>> entrySet() {return backingMap.entrySet();} + + /** + * Wraps the specified map with a most recently used cache + * @param map + * @param <K> + * @param <V> + * @return + */ + public static <K, V> Map<K, V> of(Map<K, V> map) { + return new MRUMapCache<K, V>(map); + } +} diff --git a/src/main/java/org/bukkit/Bukkit.java b/src/main/java/org/bukkit/Bukkit.java index e19f3d7..8d602a3 100644 --- a/src/main/java/org/bukkit/Bukkit.java +++ b/src/main/java/org/bukkit/Bukkit.java @@ -537,7 +537,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 92a5cbc..86fe389 100644 --- a/src/main/java/org/bukkit/Server.java +++ b/src/main/java/org/bukkit/Server.java @@ -949,12 +949,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 0ba9b1c..548d570 100644 --- a/src/main/java/org/bukkit/command/Command.java +++ b/src/main/java/org/bukkit/command/Command.java @@ -31,7 +31,8 @@ public abstract class Command { protected String usageMessage; private String permission; private String permissionMessage; - public org.spigotmc.CustomTimingsHandler timings; // Spigot + public co.aikar.timings.Timing timings; // Spigot + public String getTimingName() {return getName();} // Spigot protected Command(String name) { this(name, "", "/" + name, new ArrayList<String>()); @@ -45,7 +46,6 @@ public abstract class Command { this.usageMessage = usageMessage; this.aliases = aliases; this.activeAliases = new ArrayList<String>(aliases); - this.timings = new org.spigotmc.CustomTimingsHandler("** Command: " + name); // Spigot } /** @@ -229,7 +229,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 a08a49d..a300ae7 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().trim(); fallbackPrefix = fallbackPrefix.toLowerCase().trim(); boolean registered = register(label, command, false, fallbackPrefix); 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 fc59aa3..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<String> TIMINGS_SUBCOMMANDS = ImmutableList.of("report", "reset", "on", "off", "paste"); // Spigot - public static long timingStart = 0; // Spigot - - public TimingsCommand(String name) { - super(name); - this.description = "Manages Spigot Timings data to see performance of the server."; // Spigot - this.usageMessage = "/timings <reset|report|on|off|paste>"; // Spigot - this.setPermission("bukkit.command.timings"); - } - - // Spigot start - redesigned Timings Command - public void executeSpigotTimings(CommandSender sender, String[] args) { - if ( "on".equals( args[0] ) ) - { - ( (SimplePluginManager) Bukkit.getPluginManager() ).useTimings( true ); - CustomTimingsHandler.reload(); - sender.sendMessage( "Enabled Timings & Reset" ); - return; - } else if ( "off".equals( args[0] ) ) - { - ( (SimplePluginManager) Bukkit.getPluginManager() ).useTimings( false ); - sender.sendMessage( "Disabled Timings" ); - return; - } - - if ( !Bukkit.getPluginManager().useTimings() ) - { - sender.sendMessage( "Please enable timings by typing /timings on" ); - return; - } - - boolean paste = "paste".equals( args[0] ); - if ("reset".equals(args[0])) { - CustomTimingsHandler.reload(); - sender.sendMessage("Timings reset"); - } else if ("merged".equals(args[0]) || "report".equals(args[0]) || paste) { - long sampleTime = System.nanoTime() - timingStart; - int index = 0; - File timingFolder = new File("timings"); - timingFolder.mkdirs(); - File timings = new File(timingFolder, "timings.txt"); - ByteArrayOutputStream bout = ( paste ) ? new ByteArrayOutputStream() : null; - while (timings.exists()) timings = new File(timingFolder, "timings" + (++index) + ".txt"); - PrintStream fileTimings = null; - try { - fileTimings = ( paste ) ? new PrintStream( bout ) : new PrintStream( timings ); - - CustomTimingsHandler.printTimings(fileTimings); - fileTimings.println( "Sample time " + sampleTime + " (" + sampleTime / 1E9 + "s)" ); - - fileTimings.println( "<spigotConfig>" ); - fileTimings.println( Bukkit.spigot().getConfig().saveToString() ); - fileTimings.println( "</spigotConfig>" ); - - if ( paste ) - { - new PasteThread( sender, bout ).start(); - return; - } - - sender.sendMessage("Timings written to " + timings.getPath()); - sender.sendMessage( "Paste contents of file into form at http://www.spigotmc.org/go/timings to read results." ); - - } catch (IOException e) { - } finally { - if (fileTimings != null) { - fileTimings.close(); - } - } - } - } - // Spigot end - - @Override - public boolean execute(CommandSender sender, String currentAlias, String[] args) { - if (!testPermission(sender)) return true; - if (args.length < 1) { // Spigot - sender.sendMessage(ChatColor.RED + "Usage: " + usageMessage); - return false; - } - if (true) { executeSpigotTimings(sender, args); return true; } // Spigot - if (!sender.getServer().getPluginManager().useTimings()) { - sender.sendMessage("Please enable timings by setting \"settings.plugin-profiling\" to true in bukkit.yml"); - return true; - } - - boolean separate = "separate".equalsIgnoreCase(args[0]); - if ("reset".equalsIgnoreCase(args[0])) { - for (HandlerList handlerList : HandlerList.getHandlerLists()) { - for (RegisteredListener listener : handlerList.getRegisteredListeners()) { - if (listener instanceof TimedRegisteredListener) { - ((TimedRegisteredListener)listener).reset(); - } - } - } - sender.sendMessage("Timings reset"); - } else if ("merged".equalsIgnoreCase(args[0]) || separate) { - - int index = 0; - int pluginIdx = 0; - File timingFolder = new File("timings"); - timingFolder.mkdirs(); - File timings = new File(timingFolder, "timings.txt"); - File names = null; - while (timings.exists()) timings = new File(timingFolder, "timings" + (++index) + ".txt"); - PrintStream fileTimings = null; - PrintStream fileNames = null; - try { - fileTimings = new PrintStream(timings); - if (separate) { - names = new File(timingFolder, "names" + index + ".txt"); - fileNames = new PrintStream(names); - } - for (Plugin plugin : Bukkit.getPluginManager().getPlugins()) { - pluginIdx++; - long totalTime = 0; - if (separate) { - fileNames.println(pluginIdx + " " + plugin.getDescription().getFullName()); - fileTimings.println("Plugin " + pluginIdx); - } - else fileTimings.println(plugin.getDescription().getFullName()); - for (RegisteredListener listener : HandlerList.getRegisteredListeners(plugin)) { - if (listener instanceof TimedRegisteredListener) { - TimedRegisteredListener trl = (TimedRegisteredListener) listener; - long time = trl.getTotalTime(); - int count = trl.getCount(); - if (count == 0) continue; - long avg = time / count; - totalTime += time; - Class<? extends Event> eventClass = trl.getEventClass(); - if (count > 0 && eventClass != null) { - fileTimings.println(" " + eventClass.getSimpleName() + (trl.hasMultiple() ? " (and sub-classes)" : "") + " Time: " + time + " Count: " + count + " Avg: " + avg); - } - } - } - fileTimings.println(" Total time " + totalTime + " (" + totalTime / 1000000000 + "s)"); - } - sender.sendMessage("Timings written to " + timings.getPath()); - if (separate) sender.sendMessage("Names written to " + names.getPath()); - } catch (IOException e) { - } finally { - if (fileTimings != null) { - fileTimings.close(); - } - if (fileNames != null) { - fileNames.close(); - } - } - } else { - sender.sendMessage(ChatColor.RED + "Usage: " + usageMessage); - return false; - } - return true; - } - - @Override - public List<String> tabComplete(CommandSender sender, String alias, String[] args) { - Validate.notNull(sender, "Sender cannot be null"); - Validate.notNull(args, "Arguments cannot be null"); - Validate.notNull(alias, "Alias cannot be null"); - - if (args.length == 1) { - return StringUtil.copyPartialMatches(args[0], TIMINGS_SUBCOMMANDS, new ArrayList<String>(TIMINGS_SUBCOMMANDS.size())); - } - return ImmutableList.of(); - } - - // Spigot start - private static class PasteThread extends Thread - { - - private final CommandSender sender; - private final ByteArrayOutputStream bout; - - public PasteThread(CommandSender sender, ByteArrayOutputStream bout) - { - super( "Timings paste thread" ); - this.sender = sender; - this.bout = bout; - } - - @Override - public synchronized void start() { - if (sender instanceof RemoteConsoleCommandSender) { - run(); - } else { - super.start(); - } - } - - @Override - public void run() - { - try - { - HttpURLConnection con = (HttpURLConnection) new URL( "http://paste.ubuntu.com/" ).openConnection(); - con.setDoOutput( true ); - con.setRequestMethod( "POST" ); - con.setInstanceFollowRedirects( false ); - - OutputStream out = con.getOutputStream(); - out.write( "poster=Spigot&syntax=text&content=".getBytes( "UTF-8" ) ); - out.write( URLEncoder.encode( bout.toString( "UTF-8" ), "UTF-8" ).getBytes( "UTF-8" ) ); - out.close(); - con.getInputStream().close(); - - String location = con.getHeaderField( "Location" ); - String pasteID = location.substring( "http://paste.ubuntu.com/".length(), location.length() - 1 ); - sender.sendMessage( ChatColor.GREEN + "Timings results can be viewed at http://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 ed8b8f1..855bde5 100644 --- a/src/main/java/org/bukkit/entity/Player.java +++ b/src/main/java/org/bukkit/entity/Player.java @@ -1384,6 +1384,11 @@ public interface Player extends HumanEntity, Conversable, CommandSender, Offline public void sendMessage(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 c9d23d6..1325b03 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(), 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()); // 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 7bf2fa6..4983ea8 100644 --- a/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java +++ b/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java @@ -39,7 +39,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; /** @@ -50,7 +49,6 @@ public final class JavaPluginLoader implements PluginLoader { private final Pattern[] fileFilters = new Pattern[] { Pattern.compile("\\.jar$"), }; private final Map<String, Class<?>> classes = new java.util.concurrent.ConcurrentHashMap<String, Class<?>>(); // Spigot private final Map<String, PluginClassLoader> loaders = new LinkedHashMap<String, PluginClassLoader>(); - public static final CustomTimingsHandler pluginParentTimer = new CustomTimingsHandler("** Plugins"); // Spigot /** * This class was not meant to be constructed explicitly @@ -293,26 +291,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<String, Class<?>> classes = new java.util.concurrent.ConcurrentHashMap<String, Class<?>>(); // Spigot private final PluginDescriptionFile description; diff --git a/src/main/java/org/bukkit/util/CachedServerIcon.java b/src/main/java/org/bukkit/util/CachedServerIcon.java index 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 <http://aikar.co> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ package org.spigotmc; -import org.bukkit.command.defaults.TimingsCommand; -import org.bukkit.event.HandlerList; +import org.bukkit.Bukkit; +import org.bukkit.plugin.AuthorNagException; import org.bukkit.plugin.Plugin; -import org.bukkit.plugin.RegisteredListener; -import org.bukkit.plugin.TimedRegisteredListener; -import java.io.PrintStream; -import java.util.Collection; -import java.util.HashSet; -import java.util.List; -import java.util.Queue; -import java.util.concurrent.ConcurrentLinkedQueue; +import co.aikar.timings.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<CustomTimingsHandler> HANDLERS = new ConcurrentLinkedQueue<CustomTimingsHandler>(); - /*========================================================================*/ - private final String name; - private final CustomTimingsHandler parent; - private long count = 0; - private long start = 0; - private long timingDepth = 0; - private long totalTime = 0; - private long curTickTotal = 0; - private long violations = 0; + public CustomTimingsHandler(String name) { + 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.7.2