├── .github └── dependabot.yml ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── pom.xml └── src ├── main ├── java │ └── com │ │ └── github │ │ └── games647 │ │ └── lagmonitor │ │ ├── LagMonitor.java │ │ ├── MethodMeasurement.java │ │ ├── NativeManager.java │ │ ├── Pages.java │ │ ├── command │ │ ├── EnvironmentCommand.java │ │ ├── GraphCommand.java │ │ ├── HelpCommand.java │ │ ├── LagCommand.java │ │ ├── MbeanCommand.java │ │ ├── MonitorCommand.java │ │ ├── NativeCommand.java │ │ ├── NetworkCommand.java │ │ ├── PaginationCommand.java │ │ ├── StackTraceCommand.java │ │ ├── VmCommand.java │ │ ├── dump │ │ │ ├── DumpCommand.java │ │ │ ├── FlightCommand.java │ │ │ ├── HeapCommand.java │ │ │ └── ThreadCommand.java │ │ ├── minecraft │ │ │ ├── PingCommand.java │ │ │ ├── SystemCommand.java │ │ │ ├── TPSCommand.java │ │ │ └── TasksCommand.java │ │ └── timing │ │ │ ├── PaperTimingsCommand.java │ │ │ ├── SpigotTimingsCommand.java │ │ │ ├── Timing.java │ │ │ └── TimingCommand.java │ │ ├── graph │ │ ├── ClassesGraph.java │ │ ├── CombinedGraph.java │ │ ├── CpuGraph.java │ │ ├── GraphRenderer.java │ │ ├── HeapGraph.java │ │ └── ThreadsGraph.java │ │ ├── listener │ │ ├── BlockingConnectionSelector.java │ │ ├── GraphListener.java │ │ ├── PageManager.java │ │ └── ThreadSafetyListener.java │ │ ├── logging │ │ ├── ForwardLogService.java │ │ └── ForwardingLoggerFactory.java │ │ ├── ping │ │ ├── PaperPing.java │ │ ├── PingFetcher.java │ │ ├── ReflectionPing.java │ │ └── SpigotPing.java │ │ ├── storage │ │ ├── MonitorSaveTask.java │ │ ├── NativeSaveTask.java │ │ ├── PlayerData.java │ │ ├── Storage.java │ │ ├── TPSSaveTask.java │ │ └── WorldData.java │ │ ├── task │ │ ├── IODetectorTask.java │ │ ├── MonitorTask.java │ │ ├── PingManager.java │ │ └── TPSHistoryTask.java │ │ ├── threading │ │ ├── BlockingActionManager.java │ │ ├── BlockingSecurityManager.java │ │ ├── Injectable.java │ │ └── PluginViolation.java │ │ ├── traffic │ │ ├── CleanUpTask.java │ │ ├── Reflection.java │ │ ├── TinyProtocol.java │ │ └── TrafficReader.java │ │ └── util │ │ ├── JavaVersion.java │ │ ├── LagUtils.java │ │ └── RollingOverHistory.java └── resources │ ├── META-INF │ └── services │ │ └── org.slf4j.spi.SLF4JServiceProvider │ ├── config.yml │ ├── create.sql │ ├── default.jfc │ └── plugin.yml └── test └── java └── com └── github └── games647 └── lagmonitor ├── LagMonitorTest.java ├── RollingOverHistoryTest.java ├── listener └── BlockingConnectionSelectorTest.java └── util ├── JavaVersionTest.java └── LagUtilsTest.java /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: maven 4 | directory: "/" 5 | schedule: 6 | interval: monthly 7 | open-pull-requests-limit: 10 8 | ignore: 9 | - dependency-name: com.github.oshi:oshi-demo 10 | versions: 11 | - "> 5.2.2, < 5.3" 12 | - dependency-name: com.github.oshi:oshi-demo 13 | versions: 14 | - "> 5.3.4, < 5.4" 15 | - dependency-name: io.netty:netty-codec 16 | versions: 17 | - "> 4.1.45.Final" 18 | - dependency-name: mysql:mysql-connector-java 19 | versions: 20 | - "> 5.1.48" 21 | - dependency-name: org.apache.maven.plugins:maven-shade-plugin 22 | versions: 23 | - "> 3.2.3, < 3.3" 24 | - dependency-name: org.apache.maven.plugins:maven-surefire-plugin 25 | versions: 26 | - "> 2.22.0, < 2.23" 27 | - dependency-name: org.mockito:mockito-junit-jupiter 28 | versions: 29 | - "> 3.4.4, < 3.5" 30 | - dependency-name: org.mockito:mockito-junit-jupiter 31 | versions: 32 | - ">= 3.5.a, < 3.6" 33 | - dependency-name: pl.project13.maven:git-commit-id-plugin 34 | versions: 35 | - "> 4.0.0, < 4.1" 36 | - dependency-name: com.github.oshi:oshi-demo 37 | versions: 38 | - 5.4.1 39 | - 5.5.0 40 | - 5.5.1 41 | - 5.6.1 42 | - 5.7.0 43 | - dependency-name: org.mockito:mockito-junit-jupiter 44 | versions: 45 | - 3.7.7 46 | - 3.8.0 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Eclipse 2 | .classpath 3 | .project 4 | .settings/ 5 | 6 | # NetBeans 7 | nbproject/ 8 | nb-configuration.xml 9 | 10 | # IntelliJ 11 | *.iml 12 | *.ipr 13 | *.iws 14 | .idea/ 15 | 16 | # Maven 17 | target/ 18 | pom.xml.versionsBackup 19 | 20 | # Gradle 21 | .gradle 22 | 23 | # Ignore Gradle GUI config 24 | gradle-app.setting 25 | 26 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 27 | !gradle-wrapper.jar 28 | 29 | # various other potential build files 30 | build/ 31 | bin/ 32 | dist/ 33 | manifest.mf 34 | *.log 35 | 36 | # Vim 37 | .*.sw[a-p] 38 | 39 | # virtual machine crash logs, see https://www.java.com/en/download/help/error_hotspot.xml 40 | hs_err_pid* 41 | 42 | # Mac filesystem dust 43 | .DS_Store 44 | 45 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # Use https://travis-ci.org/ for automatic testing 2 | 3 | # speed up testing https://blog.travis-ci.com/2014-12-17-faster-builds-with-container-based-infrastructure/ 4 | sudo: false 5 | 6 | # This is a java project 7 | language: java 8 | 9 | script: mvn test -B 10 | 11 | jdk: 12 | - oraclejdk8 13 | - oraclejdk9 14 | 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016-2018 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LagMonitor 2 | 3 | ## Description 4 | 5 | Gives you the possibility to monitor your server performance. This plugin is based on the powerful tools VisualVM and 6 | Java Mission Control, both provided by Oracle. This plugin gives you the possibility to use the features provided by 7 | these tools also in Minecraft itself. This might be useful for server owners/administrators who cannot use the tools. 8 | 9 | Furthermore, it is especially made for Minecraft itself. So you can also check your TPS (Ticks per second), player ping, 10 | server timings and so on. 11 | 12 | ## Features 13 | 14 | * Player ping 15 | * Offline Java version checker 16 | * Thread safety checks 17 | * Many details about your setup like Hardware (Disk, Processor, ...) and about your OS 18 | * Sample CPU usage 19 | * Analyze RAM usage 20 | * Access to Stacktraces of running threads 21 | * Shows your ticks per second with history 22 | * Shows system performance usage 23 | * Visual graphs in-game 24 | * In-game timings viewer 25 | * Access to Java environment variables (mbeans) 26 | * Plugin specific profiles 27 | * Blocking operations on the main thread check 28 | * Make Heap and Thread dumps 29 | * Create Java Flight Recorder dump and analyze it later on your own computer 30 | * Log the server performance into a MySQL/MariaDB database 31 | 32 | ## Requirements 33 | 34 | * Java 8+ 35 | * Spigot 1.8.8+ or a fork of it (ex: Paper) 36 | 37 | ## Permissions 38 | 39 | lagmonitor.* - Access to all LagMonitor features 40 | 41 | lagmonitor.commands.* - Access to all commands 42 | 43 | ### All command permissions 44 | 45 | * lagmonitor.command.ping 46 | * lagmonitor.command.ping.other 47 | * lagmonitor.command.stacktrace 48 | * lagmonitor.command.thread 49 | * lagmonitor.command.tps 50 | * lagmonitor.command.mbean 51 | * lagmonitor.command.system 52 | * lagmonitor.command.environment 53 | * lagmonitor.command.timing 54 | * lagmonitor.command.monitor 55 | * lagmonitor.command.graph 56 | * lagmonitor.command.native 57 | * lagmonitor.command.vm 58 | * lagmonitor.command.network 59 | * lagmonitor.command.tasks 60 | * lagmonitor.command.heap 61 | * lagmonitor.command.jfr 62 | 63 | ## Commands 64 | 65 | /ping - Gets your server ping 66 | /ping - Gets the ping of the selected player 67 | /stacktrace - Gets the execution stacktrace of the current thread 68 | /stacktrace - Gets the execution stacktrace of selected thread 69 | /thread - Outputs all running threads with their current state 70 | /tpshistory - Outputs the current tps 71 | /mbean - List all available mbeans (java environment information, JMX) 72 | /mbean - List all available attributes of this mbean 73 | /mbean - Outputs the value of this attribute 74 | /system - Gives you some general information (Minecraft server related) 75 | /env - Gives you some general information (OS related) 76 | /timing - Outputs your server timings ingame 77 | /monitor [start|stop|paste] - Monitors the CPU usage of methods 78 | /graph [heap|cpu|threads|classes] - Gives you visual graph about your server (currently only the heap usage) 79 | /native - Gives you some native os information 80 | /vm - Outputs vm specific information like garbage collector, class loading or vm specification 81 | /network - Shows network interface configuration 82 | /tasks - Information about running and pending tasks 83 | /heap - Heap dump about your current memory 84 | /lagpage - Pagination command for the current pagination session 85 | /jfr - Manages the Java Flight Recordings of the native Java VM. It gives you much more detailed 86 | information including network communications, file read/write times, detailed heap and thread data, ... 87 | 88 | ## Development builds 89 | 90 | Development builds of this project can be acquired at the provided CI (continuous integration) server. It contains the 91 | latest changes from the Source-Code in preparation for the following release. This means they could contain new 92 | features, bug fixes and other changes since the last release. 93 | 94 | Nevertheless builds are only tested using a small set of automated and a few manual tests. Therefore they **could** 95 | contain new bugs and are likely to be less stable than released versions. 96 | 97 | https://ci.codemc.org/job/Games647/job/LagMonitor/changes 98 | 99 | ## Network requests 100 | 101 | This plugin performs network requests to: 102 | 103 | * https://paste.enginehub.org - uploading monitor paste command outputs 104 | 105 | ## Reproducible builds 106 | 107 | This project supports reproducible builds for enhanced security. In short, this means that the source code matches 108 | the generated built jar file. Outputs could vary by operating system (line endings), different JDK 109 | versions and build timestamp. You can extract this using 110 | [build-info](https://github.com/apache/maven-studies/tree/maven-buildinfo-plugin). Once you have 111 | the configuration to use the same line endings and JDK version, you can use the following command 112 | to inject a custom build timestamp to complete the configuration. 113 | 114 | `mvn clean install -Dproject.build.outputTimestamp=DATE` 115 | 116 | ## Images 117 | 118 | ### Heap command 119 | ![heap command](https://i.imgur.com/AzDwYxq.png) 120 | 121 | ### Timing command 122 | ![timing command](https://i.imgur.com/wAxnIxt.png) 123 | 124 | ### CPU Graph (blue=process, yellow=system) - Process load 125 | ![cpu graph](https://i.imgur.com/DajnZmP.png) 126 | 127 | ### Stacktrace and Threads command 128 | ![stacktrace and threads](https://i.imgur.com/XY7r9wz.png) 129 | 130 | ### Ping Command 131 | ![ping command](https://i.imgur.com/LITJKWw.png) 132 | 133 | ### Thread Sampler (Monitor command) 134 | ![thread sample](https://i.imgur.com/OXOakN6.png) 135 | 136 | ### System command 137 | ![system command](https://i.imgur.com/hrIV6bW.png) 138 | 139 | ### Environment command 140 | ![environment command](https://i.imgur.com/gQwr126.png) 141 | 142 | ### Heap usage graph (yellow=allocated, blue=used) 143 | ![heap usage map](https://i.imgur.com/Yiz9h6G.png) 144 | -------------------------------------------------------------------------------- /src/main/java/com/github/games647/lagmonitor/MethodMeasurement.java: -------------------------------------------------------------------------------- 1 | package com.github.games647.lagmonitor; 2 | 3 | import com.google.common.collect.ImmutableMap; 4 | 5 | import java.util.HashMap; 6 | import java.util.Map; 7 | import java.util.Objects; 8 | import java.util.stream.IntStream; 9 | 10 | public class MethodMeasurement implements Comparable { 11 | 12 | private final String id; 13 | private final String className; 14 | private final String method; 15 | 16 | private final Map childInvokes = new HashMap<>(); 17 | private long totalTime; 18 | 19 | public MethodMeasurement(String id, String className, String method) { 20 | this.id = id; 21 | 22 | this.className = className; 23 | this.method = method; 24 | } 25 | 26 | public String getId() { 27 | return id; 28 | } 29 | 30 | public String getClassName() { 31 | return className; 32 | } 33 | 34 | public String getMethod() { 35 | return method; 36 | } 37 | 38 | public long getTotalTime() { 39 | return totalTime; 40 | } 41 | 42 | public Map getChildInvokes() { 43 | return ImmutableMap.copyOf(childInvokes); 44 | } 45 | 46 | public float getTimePercent(long parentTime) { 47 | //one float conversion triggers the complete calculation to be decimal 48 | return ((float) totalTime / parentTime) * 100; 49 | } 50 | 51 | public void onMeasurement(StackTraceElement[] stackTrace, int skipElements, long time) { 52 | totalTime += time; 53 | 54 | if (skipElements >= stackTrace.length) { 55 | //we reached the end 56 | return; 57 | } 58 | 59 | StackTraceElement nextChildElement = stackTrace[stackTrace.length - skipElements - 1]; 60 | String nextClass = nextChildElement.getClassName(); 61 | String nextMethod = nextChildElement.getMethodName(); 62 | 63 | String idName = nextChildElement.getClassName() + '.' + nextChildElement.getMethodName(); 64 | MethodMeasurement child = childInvokes 65 | .computeIfAbsent(idName, (key) -> new MethodMeasurement(key, nextClass, nextMethod)); 66 | child.onMeasurement(stackTrace, skipElements + 1, time); 67 | } 68 | 69 | public void writeString(StringBuilder builder, int indent) { 70 | StringBuilder b = new StringBuilder(); 71 | IntStream.range(0, indent).forEach(i -> b.append(' ')); 72 | 73 | String padding = b.toString(); 74 | 75 | for (MethodMeasurement child : getChildInvokes().values()) { 76 | builder.append(padding).append(child.id).append("()"); 77 | builder.append(' '); 78 | builder.append(child.totalTime).append("ms"); 79 | builder.append('\n'); 80 | child.writeString(builder, indent + 1); 81 | } 82 | } 83 | 84 | @Override 85 | public boolean equals(Object o) { 86 | if (this == o) return true; 87 | if (o == null || getClass() != o.getClass()) return false; 88 | MethodMeasurement that = (MethodMeasurement) o; 89 | 90 | return totalTime == that.totalTime && 91 | Objects.equals(id, that.id) && 92 | Objects.equals(className, that.className) && 93 | Objects.equals(method, that.method) && 94 | Objects.equals(childInvokes, that.childInvokes); 95 | } 96 | 97 | @Override 98 | public int hashCode() { 99 | return Objects.hash(id, className, method, childInvokes, totalTime); 100 | } 101 | 102 | @Override 103 | public int compareTo(MethodMeasurement other) { 104 | return Long.compare(this.totalTime, other.totalTime); 105 | } 106 | 107 | @Override 108 | public String toString() { 109 | StringBuilder builder = new StringBuilder(); 110 | for (Map.Entry entry : getChildInvokes().entrySet()) { 111 | builder.append(entry.getKey()).append("()"); 112 | builder.append(' '); 113 | builder.append(entry.getValue().totalTime).append("ms"); 114 | builder.append('\n'); 115 | entry.getValue().writeString(builder, 1); 116 | } 117 | 118 | return builder.toString(); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/main/java/com/github/games647/lagmonitor/NativeManager.java: -------------------------------------------------------------------------------- 1 | package com.github.games647.lagmonitor; 2 | 3 | import com.sun.management.UnixOperatingSystemMXBean; 4 | 5 | import java.io.IOException; 6 | import java.lang.management.ManagementFactory; 7 | import java.lang.management.OperatingSystemMXBean; 8 | import java.nio.file.FileStore; 9 | import java.nio.file.Files; 10 | import java.nio.file.Path; 11 | import java.nio.file.Paths; 12 | import java.util.Optional; 13 | import java.util.logging.Level; 14 | import java.util.logging.Logger; 15 | 16 | import oshi.SystemInfo; 17 | import oshi.hardware.GlobalMemory; 18 | import oshi.software.os.OSProcess; 19 | 20 | public class NativeManager { 21 | 22 | private static final String JNA_FILE = "jna-5.5.0.jar"; 23 | 24 | private final Logger logger; 25 | private final Path dataFolder; 26 | 27 | private final OperatingSystemMXBean osBean = ManagementFactory.getOperatingSystemMXBean(); 28 | private SystemInfo info; 29 | 30 | private int pid = -1; 31 | 32 | public NativeManager(Logger logger, Path dataFolder) { 33 | this.logger = logger; 34 | this.dataFolder = dataFolder; 35 | } 36 | 37 | public void setupNativeAdapter() { 38 | logger.info("Found JNA native library. Enabling extended native data support to display more data"); 39 | try { 40 | info = new SystemInfo(); 41 | 42 | //make a test call 43 | pid = info.getOperatingSystem().getProcessId(); 44 | } catch (UnsatisfiedLinkError | NoClassDefFoundError linkError) { 45 | logger.log(Level.INFO, "Cannot load native library. Continuing without it...", linkError); 46 | info = null; 47 | } 48 | } 49 | 50 | public Optional getSystemInfo() { 51 | return Optional.ofNullable(info); 52 | } 53 | 54 | public double getProcessCPULoad() { 55 | if (osBean instanceof com.sun.management.OperatingSystemMXBean) { 56 | com.sun.management.OperatingSystemMXBean nativeOsBean = (com.sun.management.OperatingSystemMXBean) osBean; 57 | return nativeOsBean.getProcessCpuLoad(); 58 | } 59 | 60 | return -1; 61 | } 62 | 63 | public Optional getProcess() { 64 | if (info == null) { 65 | return Optional.empty(); 66 | } 67 | 68 | return Optional.of(info.getOperatingSystem().getProcess(pid)); 69 | } 70 | 71 | public double getCPULoad() { 72 | if (osBean instanceof com.sun.management.OperatingSystemMXBean) { 73 | com.sun.management.OperatingSystemMXBean nativeOsBean = (com.sun.management.OperatingSystemMXBean) osBean; 74 | return nativeOsBean.getSystemCpuLoad(); 75 | } else if (info != null) { 76 | return info.getHardware().getProcessor().getSystemLoadAverage(1)[0]; 77 | } 78 | 79 | return -1; 80 | } 81 | 82 | public long getOpenFileDescriptors() { 83 | if (osBean instanceof com.sun.management.UnixOperatingSystemMXBean) { 84 | return ((UnixOperatingSystemMXBean) osBean).getOpenFileDescriptorCount(); 85 | } else if (info != null) { 86 | return info.getOperatingSystem().getFileSystem().getOpenFileDescriptors(); 87 | } 88 | 89 | return -1; 90 | } 91 | 92 | public long getMaxFileDescriptors() { 93 | if (osBean instanceof com.sun.management.UnixOperatingSystemMXBean) { 94 | return ((UnixOperatingSystemMXBean) osBean).getMaxFileDescriptorCount(); 95 | } else if (info != null) { 96 | return info.getOperatingSystem().getFileSystem().getMaxFileDescriptors(); 97 | } 98 | 99 | return -1; 100 | } 101 | 102 | public long getTotalMemory() { 103 | if (osBean instanceof com.sun.management.OperatingSystemMXBean) { 104 | com.sun.management.OperatingSystemMXBean nativeOsBean = (com.sun.management.OperatingSystemMXBean) osBean; 105 | return nativeOsBean.getTotalPhysicalMemorySize(); 106 | } else if (info != null) { 107 | return info.getHardware().getMemory().getTotal(); 108 | } 109 | 110 | return -1; 111 | } 112 | 113 | public long getFreeMemory() { 114 | if (osBean instanceof com.sun.management.OperatingSystemMXBean) { 115 | com.sun.management.OperatingSystemMXBean nativeOsBean = (com.sun.management.OperatingSystemMXBean) osBean; 116 | return nativeOsBean.getFreePhysicalMemorySize(); 117 | } else if (info != null) { 118 | return getTotalMemory() - info.getHardware().getMemory().getAvailable(); 119 | } 120 | 121 | return -1; 122 | } 123 | 124 | public long getFreeSwap() { 125 | if (osBean instanceof com.sun.management.OperatingSystemMXBean) { 126 | com.sun.management.OperatingSystemMXBean nativeOsBean = (com.sun.management.OperatingSystemMXBean) osBean; 127 | return nativeOsBean.getFreeSwapSpaceSize(); 128 | } else if (info != null) { 129 | GlobalMemory memory = info.getHardware().getMemory(); 130 | return memory.getAvailable(); 131 | } 132 | 133 | return -1; 134 | } 135 | 136 | public long getTotalSwap() { 137 | if (osBean instanceof com.sun.management.OperatingSystemMXBean) { 138 | com.sun.management.OperatingSystemMXBean nativeOsBean = (com.sun.management.OperatingSystemMXBean) osBean; 139 | return nativeOsBean.getTotalSwapSpaceSize(); 140 | } else if (info != null) { 141 | return info.getHardware().getMemory().getVirtualMemory().getSwapTotal(); 142 | } 143 | 144 | return -1; 145 | } 146 | 147 | public long getFreeSpace() { 148 | long freeSpace = 0; 149 | try { 150 | FileStore fileStore = Files.getFileStore(Paths.get(".")); 151 | freeSpace = fileStore.getUsableSpace(); 152 | } catch (IOException ioEx) { 153 | logger.log(Level.WARNING, "Cannot calculate free/total disk space", ioEx); 154 | } 155 | 156 | return freeSpace; 157 | } 158 | 159 | public long getTotalSpace() { 160 | long totalSpace = 0; 161 | try { 162 | FileStore fileStore = Files.getFileStore(Paths.get(".")); 163 | totalSpace = fileStore.getTotalSpace(); 164 | } catch (IOException ioEx) { 165 | logger.log(Level.WARNING, "Cannot calculate free disk space", ioEx); 166 | } 167 | 168 | return totalSpace; 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/main/java/com/github/games647/lagmonitor/Pages.java: -------------------------------------------------------------------------------- 1 | package com.github.games647.lagmonitor; 2 | 3 | import java.time.LocalTime; 4 | import java.time.format.DateTimeFormatter; 5 | import java.time.format.FormatStyle; 6 | import java.util.List; 7 | 8 | import net.md_5.bungee.api.ChatColor; 9 | import net.md_5.bungee.api.chat.BaseComponent; 10 | import net.md_5.bungee.api.chat.ClickEvent; 11 | import net.md_5.bungee.api.chat.ComponentBuilder; 12 | import net.md_5.bungee.api.chat.ComponentBuilder.FormatRetention; 13 | import net.md_5.bungee.api.chat.HoverEvent; 14 | 15 | import org.bukkit.command.CommandSender; 16 | import org.bukkit.entity.Player; 17 | import org.bukkit.util.ChatPaginator; 18 | 19 | public class Pages { 20 | 21 | private static final int PAGINATION_LINES = 2; 22 | 23 | private static final int CONSOLE_HEIGHT = 40 - PAGINATION_LINES; 24 | private static final int PLAYER_HEIGHT = ChatPaginator.OPEN_CHAT_PAGE_HEIGHT - PAGINATION_LINES; 25 | 26 | public static String filterPackageNames(String packageName) { 27 | String text = packageName; 28 | if (text.contains("net.minecraft.server")) { 29 | text = text.replace("net.minecraft.server", "NMS"); 30 | } else if (text.contains("org.bukkit.craftbukkit")) { 31 | text = text.replace("org.bukkit.craftbukkit", "OBC"); 32 | } 33 | 34 | //IDEA: if it's a player we need to shorten the text more aggressively 35 | //maybe replacing the package with the plugin name 36 | //by getting the package name from the plugin.yml? 37 | return text; 38 | } 39 | 40 | private final String date = LocalTime.now().format(DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT)); 41 | private final String title; 42 | 43 | private final List lines; 44 | 45 | private int lastSentPage = 1; 46 | 47 | public Pages(String title, List lines) { 48 | this.title = title; 49 | this.lines = lines; 50 | } 51 | 52 | public int getTotalPages(boolean isPlayer) { 53 | if (isPlayer) { 54 | return (lines.size() / PLAYER_HEIGHT) + 1; 55 | } 56 | 57 | return (lines.size() / CONSOLE_HEIGHT) + 1; 58 | } 59 | 60 | public List getAllLines() { 61 | return lines; 62 | } 63 | 64 | public int getLastSentPage() { 65 | return lastSentPage; 66 | } 67 | 68 | public void setLastSentPage(int lastSentPage) { 69 | this.lastSentPage = lastSentPage; 70 | } 71 | 72 | public List getPage(int page, boolean isPlayer) { 73 | int startIndex; 74 | int endIndex; 75 | if (isPlayer) { 76 | startIndex = (page - 1) * PLAYER_HEIGHT; 77 | endIndex = page * PLAYER_HEIGHT; 78 | } else { 79 | startIndex = (page - 1) * CONSOLE_HEIGHT; 80 | endIndex = page * CONSOLE_HEIGHT; 81 | } 82 | 83 | if (startIndex >= lines.size()) { 84 | endIndex = lines.size() - 1; 85 | startIndex = endIndex; 86 | } else if (endIndex >= lines.size()) { 87 | endIndex = lines.size() - 1; 88 | } 89 | 90 | return lines.subList(startIndex, endIndex); 91 | } 92 | 93 | public BaseComponent[] buildHeader(int page, int totalPages) { 94 | return new ComponentBuilder(title + " from " + date) 95 | .color(ChatColor.GOLD) 96 | .append(" << ") 97 | .event(new HoverEvent(HoverEvent.Action.SHOW_TEXT 98 | , new ComponentBuilder("Go to the previous page").create())) 99 | .event(new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/lagpage " + (page - 1))) 100 | .color(ChatColor.DARK_AQUA) 101 | .append(page + " / " + totalPages, FormatRetention.NONE) 102 | .color(ChatColor.GRAY) 103 | .append(" >>") 104 | .event(new HoverEvent(HoverEvent.Action.SHOW_TEXT 105 | , new ComponentBuilder("Go to the next page").create())) 106 | .event(new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/lagpage " + (page + 1))) 107 | .color(ChatColor.DARK_AQUA) 108 | .create(); 109 | } 110 | 111 | public String buildFooter(int page, boolean isPlayer) { 112 | int endIndex; 113 | if (isPlayer) { 114 | endIndex = page * PLAYER_HEIGHT; 115 | } else { 116 | endIndex = page * CONSOLE_HEIGHT; 117 | } 118 | 119 | if (endIndex < lines.size()) { 120 | //Index starts by 0 121 | int remaining = lines.size() - endIndex - 1; 122 | return "... " + remaining + " more entries. Click the arrows above or type /lagpage next"; 123 | } 124 | 125 | return ""; 126 | } 127 | 128 | public void send(CommandSender sender) { 129 | send(sender, 1); 130 | } 131 | 132 | public void send(CommandSender sender, int page) { 133 | this.lastSentPage = page; 134 | 135 | if (sender instanceof Player) { 136 | Player player = (Player) sender; 137 | player.spigot().sendMessage(buildHeader(page, getTotalPages(true))); 138 | 139 | getPage(page, true).forEach(player.spigot()::sendMessage); 140 | 141 | String footer = buildFooter(page, true); 142 | if (!footer.isEmpty()) { 143 | sender.sendMessage(ChatColor.GOLD + footer); 144 | } 145 | } else { 146 | BaseComponent[] header = buildHeader(page, getTotalPages(false)); 147 | StringBuilder headerBuilder = new StringBuilder(); 148 | for (BaseComponent component : header) { 149 | headerBuilder.append(component.toLegacyText()); 150 | } 151 | 152 | sender.sendMessage(headerBuilder.toString()); 153 | getPage(page, false).stream().map(line -> { 154 | StringBuilder lineBuilder = new StringBuilder(); 155 | for (BaseComponent component : line) { 156 | lineBuilder.append(component.toLegacyText()); 157 | } 158 | 159 | return lineBuilder.toString(); 160 | }).forEach(sender::sendMessage); 161 | 162 | String footer = buildFooter(page, false); 163 | if (!footer.isEmpty()) { 164 | sender.sendMessage(ChatColor.GOLD + footer); 165 | } 166 | } 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /src/main/java/com/github/games647/lagmonitor/command/EnvironmentCommand.java: -------------------------------------------------------------------------------- 1 | package com.github.games647.lagmonitor.command; 2 | 3 | import com.github.games647.lagmonitor.LagMonitor; 4 | import com.github.games647.lagmonitor.NativeManager; 5 | 6 | import java.lang.management.ManagementFactory; 7 | import java.lang.management.OperatingSystemMXBean; 8 | import java.text.DecimalFormat; 9 | import java.util.Map.Entry; 10 | import java.util.Optional; 11 | 12 | import oshi.SystemInfo; 13 | import oshi.hardware.CentralProcessor; 14 | import oshi.hardware.CentralProcessor.ProcessorIdentifier; 15 | import oshi.software.os.OperatingSystem; 16 | 17 | import org.bukkit.command.Command; 18 | import org.bukkit.command.CommandSender; 19 | 20 | import static com.github.games647.lagmonitor.util.LagUtils.readableBytes; 21 | 22 | public class EnvironmentCommand extends LagCommand { 23 | 24 | public EnvironmentCommand(LagMonitor plugin) { 25 | super(plugin); 26 | } 27 | 28 | @Override 29 | public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { 30 | if (!canExecute(sender, command)) { 31 | return true; 32 | } 33 | 34 | OperatingSystemMXBean osBean = ManagementFactory.getOperatingSystemMXBean(); 35 | 36 | //os general info 37 | sendMessage(sender, "OS Name", osBean.getName()); 38 | sendMessage(sender, "OS Arch", osBean.getArch()); 39 | 40 | Optional optInfo = plugin.getNativeData().getSystemInfo(); 41 | if (optInfo.isPresent()) { 42 | SystemInfo systemInfo = optInfo.get(); 43 | 44 | OperatingSystem osInfo = systemInfo.getOperatingSystem(); 45 | sendMessage(sender, "OS family", osInfo.getFamily()); 46 | sendMessage(sender, "OS version", osInfo.getVersionInfo().toString()); 47 | sendMessage(sender, "OS Manufacturer", osInfo.getManufacturer()); 48 | 49 | sendMessage(sender, "Total processes", String.valueOf(osInfo.getProcessCount())); 50 | sendMessage(sender, "Total threads", String.valueOf(osInfo.getThreadCount())); 51 | } 52 | 53 | //CPU 54 | sender.sendMessage(PRIMARY_COLOR + "CPU:"); 55 | if (optInfo.isPresent()) { 56 | CentralProcessor processor = optInfo.get().getHardware().getProcessor(); 57 | ProcessorIdentifier identifier = processor.getProcessorIdentifier(); 58 | 59 | sendMessage(sender, " Vendor", identifier.getVendor()); 60 | sendMessage(sender, " Family", identifier.getFamily()); 61 | sendMessage(sender, " Name", identifier.getName()); 62 | sendMessage(sender, " Model", identifier.getModel()); 63 | sendMessage(sender, " Id", identifier.getIdentifier()); 64 | sendMessage(sender, " Vendor freq", String.valueOf(identifier.getVendorFreq())); 65 | sendMessage(sender, " Physical Cores", String.valueOf(processor.getPhysicalProcessorCount())); 66 | } 67 | 68 | sendMessage(sender, " Logical Cores", String.valueOf(osBean.getAvailableProcessors())); 69 | sendMessage(sender, " Endian", System.getProperty("sun.cpu.endian", "Unknown")); 70 | 71 | sendMessage(sender, "Load Average", String.valueOf(osBean.getSystemLoadAverage())); 72 | printExtendOsInfo(sender); 73 | 74 | displayDiskSpace(sender); 75 | 76 | NativeManager nativeData = plugin.getNativeData(); 77 | sendMessage(sender, "Open file descriptors", String.valueOf(nativeData.getOpenFileDescriptors())); 78 | sendMessage(sender, "Max file descriptors", String.valueOf(nativeData.getMaxFileDescriptors())); 79 | 80 | sender.sendMessage(PRIMARY_COLOR + "Variables:"); 81 | for (Entry variable : System.getenv().entrySet()) { 82 | sendMessage(sender, " " + variable.getKey(), variable.getValue()); 83 | } 84 | 85 | return true; 86 | } 87 | 88 | private void printExtendOsInfo(CommandSender sender) { 89 | NativeManager nativeData = plugin.getNativeData(); 90 | 91 | //cpu 92 | double systemCpuLoad = nativeData.getCPULoad(); 93 | double processCpuLoad = nativeData.getProcessCPULoad(); 94 | 95 | //these numbers are in percent (0.01 -> 1%) 96 | //we want to to have four places in a human readable percent value to multiple it with 100 97 | DecimalFormat decimalFormat = new DecimalFormat("###.#### %"); 98 | decimalFormat.setMultiplier(100); 99 | String systemLoadFormat = decimalFormat.format(systemCpuLoad); 100 | String processLoadFormat = decimalFormat.format(processCpuLoad); 101 | 102 | sendMessage(sender,"System Usage", systemLoadFormat); 103 | sendMessage(sender,"Process Usage", processLoadFormat); 104 | 105 | //swap 106 | long totalSwap = nativeData.getTotalSwap(); 107 | long freeSwap = nativeData.getFreeSwap(); 108 | sendMessage(sender, "Total Swap", readableBytes(totalSwap)); 109 | sendMessage(sender, "Free Swap", readableBytes(freeSwap)); 110 | 111 | //RAM 112 | long totalMemory = nativeData.getTotalMemory(); 113 | long freeMemory = nativeData.getFreeMemory(); 114 | sendMessage(sender, "Total OS RAM", readableBytes(totalMemory)); 115 | sendMessage(sender, "Free OS RAM", readableBytes(freeMemory)); 116 | } 117 | 118 | private void displayDiskSpace(CommandSender sender) { 119 | long freeSpace = plugin.getNativeData().getFreeSpace(); 120 | long totalSpace = plugin.getNativeData().getTotalSpace(); 121 | 122 | //Disk info 123 | sendMessage(sender,"Disk Size", readableBytes(totalSpace)); 124 | sendMessage(sender,"Free Space", readableBytes(freeSpace)); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/main/java/com/github/games647/lagmonitor/command/GraphCommand.java: -------------------------------------------------------------------------------- 1 | package com.github.games647.lagmonitor.command; 2 | 3 | import com.github.games647.lagmonitor.LagMonitor; 4 | import com.github.games647.lagmonitor.graph.ClassesGraph; 5 | import com.github.games647.lagmonitor.graph.CombinedGraph; 6 | import com.github.games647.lagmonitor.graph.CpuGraph; 7 | import com.github.games647.lagmonitor.graph.GraphRenderer; 8 | import com.github.games647.lagmonitor.graph.HeapGraph; 9 | import com.github.games647.lagmonitor.graph.ThreadsGraph; 10 | import com.github.games647.lagmonitor.util.LagUtils; 11 | 12 | import java.util.ArrayList; 13 | import java.util.Collections; 14 | import java.util.HashMap; 15 | import java.util.List; 16 | import java.util.Map; 17 | 18 | import org.bukkit.Bukkit; 19 | import org.bukkit.ChatColor; 20 | import org.bukkit.Material; 21 | import org.bukkit.command.Command; 22 | import org.bukkit.command.CommandSender; 23 | import org.bukkit.command.TabExecutor; 24 | import org.bukkit.entity.Player; 25 | import org.bukkit.inventory.ItemStack; 26 | import org.bukkit.inventory.PlayerInventory; 27 | import org.bukkit.inventory.meta.ItemMeta; 28 | import org.bukkit.inventory.meta.MapMeta; 29 | import org.bukkit.map.MapView; 30 | 31 | import static java.util.stream.Collectors.toList; 32 | 33 | public class GraphCommand extends LagCommand implements TabExecutor { 34 | 35 | private static final int MAX_COMBINED = 4; 36 | 37 | private final Map graphTypes = new HashMap<>(); 38 | 39 | public GraphCommand(LagMonitor plugin) { 40 | super(plugin); 41 | 42 | graphTypes.put("classes", new ClassesGraph()); 43 | graphTypes.put("cpu", new CpuGraph(plugin, plugin.getNativeData())); 44 | graphTypes.put("heap", new HeapGraph()); 45 | graphTypes.put("threads", new ThreadsGraph()); 46 | } 47 | 48 | @Override 49 | public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { 50 | if (!canExecute(sender, command)) { 51 | return true; 52 | } 53 | 54 | if (sender instanceof Player) { 55 | Player player = (Player) sender; 56 | 57 | if (args.length > 0) { 58 | if (args.length > 1) { 59 | buildCombinedGraph(player, args); 60 | } else { 61 | String graph = args[0]; 62 | GraphRenderer renderer = graphTypes.get(graph); 63 | if (renderer == null) { 64 | sendError(sender, "Unknown graph type"); 65 | } else { 66 | giveMap(player, installRenderer(player, renderer)); 67 | } 68 | } 69 | 70 | return true; 71 | } 72 | 73 | //default is heap usage 74 | GraphRenderer graphRenderer = graphTypes.get("heap"); 75 | MapView mapView = installRenderer(player, graphRenderer); 76 | giveMap(player, mapView); 77 | } else { 78 | sendError(sender, "Not implemented for the console"); 79 | } 80 | 81 | return true; 82 | } 83 | 84 | @Override 85 | public List onTabComplete(CommandSender sender, Command command, String alias, String[] args) { 86 | if (args.length != 1) { 87 | return Collections.emptyList(); 88 | } 89 | 90 | String lastArg = args[args.length - 1]; 91 | return graphTypes.keySet().stream() 92 | .filter(type -> type.startsWith(lastArg)) 93 | .sorted(String.CASE_INSENSITIVE_ORDER) 94 | .collect(toList()); 95 | } 96 | 97 | private void buildCombinedGraph(Player player, String[] args) { 98 | List renderers = new ArrayList<>(); 99 | for (String arg : args) { 100 | GraphRenderer renderer = graphTypes.get(arg); 101 | if (renderer == null) { 102 | sendError(player, "Unknown graph type " + arg); 103 | return; 104 | } 105 | 106 | renderers.add(renderer); 107 | } 108 | 109 | if (renderers.size() > MAX_COMBINED) { 110 | sendError(player, "Too many graphs"); 111 | } else { 112 | CombinedGraph combinedGraph = new CombinedGraph(renderers.toArray(new GraphRenderer[0])); 113 | MapView view = installRenderer(player, combinedGraph); 114 | giveMap(player, view); 115 | } 116 | } 117 | 118 | private void giveMap(Player player, MapView mapView) { 119 | PlayerInventory inventory = player.getInventory(); 120 | 121 | ItemStack mapItem; 122 | if (LagUtils.isFilledMapSupported()) { 123 | mapItem = new ItemStack(Material.FILLED_MAP, 1); 124 | ItemMeta meta = mapItem.getItemMeta(); 125 | if (meta instanceof MapMeta) { 126 | MapMeta mapMeta = (MapMeta) meta; 127 | mapMeta.setMapView(mapView); 128 | mapItem.setItemMeta(meta); 129 | } 130 | } else { 131 | mapItem = new ItemStack(Material.MAP, 1, (short) mapView.getId()); 132 | } 133 | 134 | inventory.addItem(mapItem); 135 | player.sendMessage(ChatColor.DARK_GREEN + "You received a map with the graph"); 136 | } 137 | 138 | private MapView installRenderer(Player player, GraphRenderer graphType) { 139 | MapView mapView = Bukkit.createMap(player.getWorld()); 140 | mapView.getRenderers().forEach(mapView::removeRenderer); 141 | 142 | mapView.addRenderer(graphType); 143 | return mapView; 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/main/java/com/github/games647/lagmonitor/command/HelpCommand.java: -------------------------------------------------------------------------------- 1 | package com.github.games647.lagmonitor.command; 2 | 3 | import com.github.games647.lagmonitor.LagMonitor; 4 | 5 | import java.util.Map; 6 | import java.util.Map.Entry; 7 | 8 | import net.md_5.bungee.api.ChatColor; 9 | import net.md_5.bungee.api.chat.ComponentBuilder; 10 | import net.md_5.bungee.api.chat.HoverEvent; 11 | import net.md_5.bungee.api.chat.HoverEvent.Action; 12 | import net.md_5.bungee.api.chat.TextComponent; 13 | 14 | import org.bukkit.command.Command; 15 | import org.bukkit.command.CommandSender; 16 | import org.bukkit.entity.Player; 17 | import org.bukkit.util.ChatPaginator; 18 | 19 | public class HelpCommand extends LagCommand { 20 | 21 | private static final int HOVER_MAX_LENGTH = 40; 22 | 23 | public HelpCommand(LagMonitor plugin) { 24 | super(plugin); 25 | } 26 | 27 | @Override 28 | public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { 29 | sender.sendMessage(ChatColor.AQUA + plugin.getName() + "-Help"); 30 | 31 | int maxWidth = ChatPaginator.GUARANTEED_NO_WRAP_CHAT_PAGE_WIDTH; 32 | if (!(sender instanceof Player)) { 33 | maxWidth = Integer.MAX_VALUE; 34 | } 35 | 36 | for (Entry> entry : plugin.getDescription().getCommands().entrySet()) { 37 | String commandKey = entry.getKey(); 38 | Map value = entry.getValue(); 39 | 40 | String description = ' ' + value.getOrDefault("description", "No description").toString(); 41 | String usage = ((String) value.getOrDefault("usage", '/' + commandKey)).replace("", commandKey); 42 | 43 | TextComponent component = createCommandHelp(usage, description, maxWidth); 44 | LagCommand.send(sender, component); 45 | } 46 | 47 | return true; 48 | } 49 | 50 | private TextComponent createCommandHelp(String usage, String description, int maxWidth) { 51 | TextComponent usageComponent = new TextComponent(usage); 52 | usageComponent.setColor(ChatColor.DARK_AQUA); 53 | 54 | TextComponent descriptionComponent = new TextComponent(description); 55 | descriptionComponent.setColor(ChatColor.GOLD); 56 | int totalLen = usage.length() + description.length(); 57 | if (totalLen > maxWidth) { 58 | int newDescLength = maxWidth - usage.length() - 3 - 1; 59 | if (newDescLength < 0) { 60 | newDescLength = 0; 61 | } 62 | 63 | String shortDesc = description.substring(0, newDescLength) + "..."; 64 | descriptionComponent.setText(shortDesc); 65 | 66 | ComponentBuilder hoverBuilder = new ComponentBuilder(""); 67 | 68 | String[] separated = ChatPaginator.wordWrap(description, HOVER_MAX_LENGTH); 69 | for (String line : separated) { 70 | hoverBuilder.append(line + '\n'); 71 | hoverBuilder.color(ChatColor.GOLD); 72 | } 73 | 74 | descriptionComponent.setHoverEvent(new HoverEvent(Action.SHOW_TEXT, hoverBuilder.create())); 75 | } else { 76 | descriptionComponent.setText(description); 77 | } 78 | 79 | usageComponent.addExtra(descriptionComponent); 80 | return usageComponent; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/main/java/com/github/games647/lagmonitor/command/LagCommand.java: -------------------------------------------------------------------------------- 1 | package com.github.games647.lagmonitor.command; 2 | 3 | import com.github.games647.lagmonitor.LagMonitor; 4 | 5 | import java.util.ArrayList; 6 | import java.util.Collection; 7 | import java.util.List; 8 | 9 | import net.md_5.bungee.api.chat.BaseComponent; 10 | import net.md_5.bungee.api.chat.TextComponent; 11 | 12 | import org.bukkit.ChatColor; 13 | import org.bukkit.command.Command; 14 | import org.bukkit.command.CommandExecutor; 15 | import org.bukkit.command.CommandSender; 16 | import org.bukkit.configuration.file.FileConfiguration; 17 | import org.bukkit.entity.Player; 18 | 19 | public abstract class LagCommand implements CommandExecutor { 20 | 21 | protected static final ChatColor PRIMARY_COLOR = ChatColor.DARK_AQUA; 22 | protected static final ChatColor SECONDARY_COLOR = ChatColor.GRAY; 23 | 24 | protected static final String NATIVE_NOT_FOUND = "Native library not found. Please download it and place it " + 25 | "inside configuration folder of this plugin to see this data"; 26 | 27 | protected final LagMonitor plugin; 28 | 29 | public LagCommand(LagMonitor plugin) { 30 | this.plugin = plugin; 31 | } 32 | 33 | private boolean isCommandAllowed(Command cmd, CommandSender sender) { 34 | if (!(sender instanceof Player)) { 35 | return true; 36 | } 37 | 38 | FileConfiguration config = plugin.getConfig(); 39 | 40 | Collection aliases = new ArrayList<>(cmd.getAliases()); 41 | aliases.add(cmd.getName()); 42 | for (String alias : aliases) { 43 | List aliasAllowed = config.getStringList("allow-" + alias); 44 | if (!aliasAllowed.isEmpty()) { 45 | return aliasAllowed.contains(sender.getName()); 46 | } 47 | } 48 | 49 | // allowlist doesn't exist 50 | return true; 51 | } 52 | 53 | public boolean canExecute(CommandSender sender, Command cmd) { 54 | if (!isCommandAllowed(cmd, sender)) { 55 | sendError(sender, "Command not allowed for you!"); 56 | return false; 57 | } 58 | 59 | return true; 60 | } 61 | 62 | protected void sendMessage(CommandSender sender, String title, String value) { 63 | sender.sendMessage(PRIMARY_COLOR + title + ": " + SECONDARY_COLOR + value); 64 | } 65 | 66 | protected void sendError(CommandSender sender, String msg) { 67 | sender.sendMessage(ChatColor.DARK_RED + msg); 68 | } 69 | 70 | public static void send(CommandSender sender, BaseComponent... components) { 71 | //CommandSender#sendMessage(BaseComponent[]) was introduced after 1.8. This is a backwards compatible solution 72 | if (sender instanceof Player) { 73 | sender.spigot().sendMessage(components); 74 | } else { 75 | sender.sendMessage(TextComponent.toLegacyText(components)); 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/main/java/com/github/games647/lagmonitor/command/MbeanCommand.java: -------------------------------------------------------------------------------- 1 | package com.github.games647.lagmonitor.command; 2 | 3 | import com.github.games647.lagmonitor.LagMonitor; 4 | 5 | import java.lang.management.ManagementFactory; 6 | import java.util.Arrays; 7 | import java.util.List; 8 | import java.util.Objects; 9 | import java.util.Set; 10 | import java.util.logging.Level; 11 | import java.util.stream.Stream; 12 | 13 | import javax.management.MBeanAttributeInfo; 14 | import javax.management.MBeanServer; 15 | import javax.management.ObjectInstance; 16 | import javax.management.ObjectName; 17 | 18 | import org.bukkit.ChatColor; 19 | import org.bukkit.command.Command; 20 | import org.bukkit.command.CommandSender; 21 | import org.bukkit.command.TabExecutor; 22 | 23 | import static java.util.stream.Collectors.toList; 24 | 25 | public class MbeanCommand extends LagCommand implements TabExecutor { 26 | 27 | public MbeanCommand(LagMonitor plugin) { 28 | super(plugin); 29 | } 30 | 31 | @Override 32 | public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { 33 | if (!canExecute(sender, command)) { 34 | return true; 35 | } 36 | 37 | MBeanServer mBeanServer = ManagementFactory.getPlatformMBeanServer(); 38 | 39 | if (args.length > 0) { 40 | try { 41 | ObjectName beanObject = ObjectName.getInstance(args[0]); 42 | if (args.length > 1) { 43 | Object result = mBeanServer.getAttribute(beanObject, args[1]); 44 | sender.sendMessage(ChatColor.DARK_GREEN + Objects.toString(result)); 45 | } else { 46 | MBeanAttributeInfo[] attributes = mBeanServer.getMBeanInfo(beanObject).getAttributes(); 47 | for (MBeanAttributeInfo attribute : attributes) { 48 | if ("ObjectName".equals(attribute.getName())) { 49 | //ignore the object name - it's already known if the user invoke the command 50 | continue; 51 | } 52 | 53 | sender.sendMessage(ChatColor.YELLOW + attribute.getName()); 54 | } 55 | } 56 | } catch (Exception ex) { 57 | plugin.getLogger().log(Level.SEVERE, null, ex); 58 | } 59 | } else { 60 | Set allBeans = mBeanServer.queryMBeans(null, null); 61 | allBeans.stream() 62 | .map(ObjectInstance::getObjectName) 63 | .map(ObjectName::getCanonicalName) 64 | .forEach(bean -> sender.sendMessage(ChatColor.DARK_AQUA + bean)); 65 | } 66 | 67 | return true; 68 | } 69 | 70 | @Override 71 | public List onTabComplete(CommandSender sender, Command command, String alias, String[] args) { 72 | String lastArg = args[args.length - 1]; 73 | 74 | MBeanServer mBeanServer = ManagementFactory.getPlatformMBeanServer(); 75 | 76 | Stream result = Stream.empty(); 77 | if (args.length == 1) { 78 | Set mbeans = mBeanServer.queryNames(null, null); 79 | result = mbeans.stream() 80 | .map(ObjectName::getCanonicalName) 81 | .filter(name -> name.startsWith(lastArg)); 82 | } else if (args.length == 2) { 83 | try { 84 | ObjectName beanObject = ObjectName.getInstance(args[0]); 85 | result = Arrays.stream(mBeanServer.getMBeanInfo(beanObject).getAttributes()) 86 | .map(MBeanAttributeInfo::getName) 87 | //ignore the object name - it's already known if the user invoke the command 88 | .filter(attribute -> !"ObjectName".equals(attribute)); 89 | } catch (Exception ex) { 90 | plugin.getLogger().log(Level.SEVERE, null, ex); 91 | } 92 | } 93 | 94 | return result.sorted(String.CASE_INSENSITIVE_ORDER).collect(toList()); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/main/java/com/github/games647/lagmonitor/command/MonitorCommand.java: -------------------------------------------------------------------------------- 1 | package com.github.games647.lagmonitor.command; 2 | 3 | import com.github.games647.lagmonitor.LagMonitor; 4 | import com.github.games647.lagmonitor.MethodMeasurement; 5 | import com.github.games647.lagmonitor.Pages; 6 | import com.github.games647.lagmonitor.task.MonitorTask; 7 | import com.google.common.base.Strings; 8 | 9 | import java.util.ArrayList; 10 | import java.util.Collection; 11 | import java.util.Collections; 12 | import java.util.List; 13 | import java.util.Timer; 14 | import java.util.concurrent.TimeUnit; 15 | 16 | import net.md_5.bungee.api.ChatColor; 17 | import net.md_5.bungee.api.chat.BaseComponent; 18 | import net.md_5.bungee.api.chat.ClickEvent; 19 | import net.md_5.bungee.api.chat.ClickEvent.Action; 20 | import net.md_5.bungee.api.chat.ComponentBuilder; 21 | 22 | import org.bukkit.Bukkit; 23 | import org.bukkit.command.Command; 24 | import org.bukkit.command.CommandSender; 25 | 26 | public class MonitorCommand extends LagCommand { 27 | 28 | public static final long SAMPLE_INTERVAL = 100L; 29 | public static final long SAMPLE_DELAY = TimeUnit.SECONDS.toMillis(1) / 2; 30 | 31 | private MonitorTask monitorTask; 32 | 33 | public MonitorCommand(LagMonitor plugin) { 34 | super(plugin); 35 | } 36 | 37 | @Override 38 | public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { 39 | if (!canExecute(sender, command)) { 40 | return true; 41 | } 42 | 43 | if (args.length > 0) { 44 | String monitorCommand = args[0].toLowerCase(); 45 | switch (monitorCommand) { 46 | case "start": 47 | startMonitor(sender); 48 | break; 49 | case "stop": 50 | stopMonitor(sender); 51 | break; 52 | case "paste": 53 | pasteMonitor(sender); 54 | break; 55 | default: 56 | sendError(sender, "Invalid command parameter"); 57 | } 58 | } else if (monitorTask == null) { 59 | sendError(sender, "Monitor is not running"); 60 | } else { 61 | List lines = new ArrayList<>(); 62 | synchronized (this) { 63 | MethodMeasurement rootSample = monitorTask.getRootSample(); 64 | printTrace(lines, 0, rootSample, 0); 65 | } 66 | 67 | Pages pagination = new Pages("Monitor", lines); 68 | pagination.send(sender); 69 | this.plugin.getPageManager().setPagination(sender.getName(), pagination); 70 | } 71 | 72 | return true; 73 | } 74 | 75 | private void printTrace(List lines, long parentTime, MethodMeasurement current, int depth) { 76 | String space = Strings.repeat(" ", depth); 77 | 78 | long currentTime = current.getTotalTime(); 79 | float timePercent = current.getTimePercent(parentTime); 80 | 81 | String clazz = Pages.filterPackageNames(current.getClassName()); 82 | String method = current.getMethod(); 83 | lines.add(new ComponentBuilder(space + "[-] ") 84 | .append(clazz + '.') 85 | .color(ChatColor.DARK_AQUA) 86 | .append(method) 87 | .color(ChatColor.DARK_GREEN) 88 | .append(' ' + timePercent + "%") 89 | .color(ChatColor.GRAY) 90 | .create()); 91 | 92 | Collection childInvokes = current.getChildInvokes().values(); 93 | List sortedList = new ArrayList<>(childInvokes); 94 | Collections.sort(sortedList); 95 | 96 | sortedList.forEach((child) -> printTrace(lines, currentTime, child, depth + 1)); 97 | } 98 | 99 | private void startMonitor(CommandSender sender) { 100 | Timer timer = plugin.getMonitorTimer(); 101 | if (monitorTask == null && timer == null) { 102 | timer = new Timer(plugin.getName() + "-Monitor"); 103 | plugin.setMonitorTimer(timer); 104 | 105 | monitorTask = new MonitorTask(plugin.getLogger(), Thread.currentThread().getId()); 106 | timer.scheduleAtFixedRate(monitorTask, SAMPLE_DELAY, SAMPLE_INTERVAL); 107 | 108 | sender.sendMessage(ChatColor.DARK_GREEN + "Monitor started"); 109 | } else { 110 | sendError(sender, "Monitor task is already running"); 111 | } 112 | } 113 | 114 | private void stopMonitor(CommandSender sender) { 115 | Timer timer = plugin.getMonitorTimer(); 116 | if (monitorTask == null && timer == null) { 117 | sendError(sender, "Monitor is not running"); 118 | } else { 119 | monitorTask = null; 120 | if (timer != null) { 121 | timer.cancel(); 122 | timer.purge(); 123 | plugin.setMonitorTimer(null); 124 | } 125 | 126 | sender.sendMessage(ChatColor.DARK_GREEN + "Monitor stopped"); 127 | } 128 | } 129 | 130 | private void pasteMonitor(final CommandSender sender) { 131 | Timer timer = plugin.getMonitorTimer(); 132 | if (monitorTask == null && timer == null) { 133 | sendError(sender, "Monitor is not running"); 134 | } 135 | 136 | Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { 137 | String reportUrl = monitorTask.paste(); 138 | if (reportUrl == null) { 139 | sendError(sender, "Error occurred. Please check the console"); 140 | } else { 141 | String profileUrl = reportUrl + ".profile"; 142 | send(sender, new ComponentBuilder("Report url: " + profileUrl) 143 | .color(ChatColor.GREEN) 144 | .event(new ClickEvent(Action.OPEN_URL, profileUrl)) 145 | .create()); 146 | } 147 | }); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/main/java/com/github/games647/lagmonitor/command/NativeCommand.java: -------------------------------------------------------------------------------- 1 | package com.github.games647.lagmonitor.command; 2 | 3 | import com.github.games647.lagmonitor.LagMonitor; 4 | import com.github.games647.lagmonitor.util.LagUtils; 5 | 6 | import java.time.Duration; 7 | import java.util.Arrays; 8 | import java.util.List; 9 | import java.util.Optional; 10 | import java.util.concurrent.TimeUnit; 11 | 12 | import oshi.SystemInfo; 13 | import oshi.demo.DetectVM; 14 | import oshi.hardware.Baseboard; 15 | import oshi.hardware.ComputerSystem; 16 | import oshi.hardware.Firmware; 17 | import oshi.hardware.HWDiskStore; 18 | import oshi.hardware.HardwareAbstractionLayer; 19 | import oshi.hardware.PhysicalMemory; 20 | import oshi.hardware.Sensors; 21 | import oshi.software.os.OSFileStore; 22 | import oshi.software.os.OperatingSystem; 23 | 24 | import org.bukkit.command.Command; 25 | import org.bukkit.command.CommandSender; 26 | 27 | public class NativeCommand extends LagCommand { 28 | 29 | public NativeCommand(LagMonitor plugin) { 30 | super(plugin); 31 | } 32 | 33 | @Override 34 | public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { 35 | if (!canExecute(sender, command)) { 36 | return true; 37 | } 38 | 39 | Optional optInfo = plugin.getNativeData().getSystemInfo(); 40 | if (optInfo.isPresent()) { 41 | displayNativeInfo(sender, optInfo.get()); 42 | } else { 43 | sendError(sender, NATIVE_NOT_FOUND); 44 | } 45 | 46 | return true; 47 | } 48 | 49 | private void displayNativeInfo(CommandSender sender, SystemInfo systemInfo) { 50 | HardwareAbstractionLayer hardware = systemInfo.getHardware(); 51 | OperatingSystem operatingSystem = systemInfo.getOperatingSystem(); 52 | 53 | //swap and load is already available in the environment command because MBeans already supports this 54 | long uptime = TimeUnit.SECONDS.toMillis(operatingSystem.getSystemUptime()); 55 | String uptimeFormat = LagMonitor.formatDuration(Duration.ofMillis(uptime)); 56 | sendMessage(sender, "OS Uptime", uptimeFormat); 57 | 58 | String startTime = LagMonitor.formatDuration(Duration.ofMillis(uptime)); 59 | sendMessage(sender, "OS Start time", startTime); 60 | 61 | sendMessage(sender, "CPU Freq", Arrays.toString(hardware.getProcessor().getCurrentFreq())); 62 | sendMessage(sender, "CPU Max Freq", String.valueOf(hardware.getProcessor().getMaxFreq())); 63 | sendMessage(sender, "VM Hypervisor", DetectVM.identifyVM()); 64 | 65 | //disk 66 | printDiskInfo(sender, hardware.getDiskStores()); 67 | displayMounts(sender, operatingSystem.getFileSystem().getFileStores()); 68 | 69 | printSensorsInfo(sender, hardware.getSensors()); 70 | printBoardInfo(sender, hardware.getComputerSystem()); 71 | 72 | printRAMInfo(sender, hardware.getMemory().getPhysicalMemory()); 73 | } 74 | 75 | private void printRAMInfo(CommandSender sender, List physicalMemories) { 76 | sender.sendMessage(PRIMARY_COLOR + "Memory:"); 77 | for (PhysicalMemory memory : physicalMemories) { 78 | sendMessage(sender, " Label", memory.getBankLabel()); 79 | sendMessage(sender, " Manufacturer", memory.getManufacturer()); 80 | sendMessage(sender, " Type", memory.getMemoryType()); 81 | sendMessage(sender, " Capacity", LagUtils.readableBytes(memory.getCapacity())); 82 | sendMessage(sender, " Clock speed", String.valueOf(memory.getClockSpeed())); 83 | } 84 | } 85 | 86 | private void printBoardInfo(CommandSender sender, ComputerSystem computerSystem) { 87 | sendMessage(sender, "System Manufacturer", computerSystem.getManufacturer()); 88 | sendMessage(sender, "System model", computerSystem.getModel()); 89 | sendMessage(sender, "Serial number", computerSystem.getSerialNumber()); 90 | 91 | sender.sendMessage(PRIMARY_COLOR + "Baseboard:"); 92 | Baseboard baseboard = computerSystem.getBaseboard(); 93 | sendMessage(sender, " Manufacturer", baseboard.getManufacturer()); 94 | sendMessage(sender, " Model", baseboard.getModel()); 95 | sendMessage(sender, " Serial", baseboard.getVersion()); 96 | sendMessage(sender, " Version", baseboard.getVersion()); 97 | 98 | sender.sendMessage(PRIMARY_COLOR + "BIOS Firmware:"); 99 | Firmware firmware = computerSystem.getFirmware(); 100 | sendMessage(sender, " Manufacturer", firmware.getManufacturer()); 101 | sendMessage(sender, " Name", firmware.getName()); 102 | sendMessage(sender, " Description", firmware.getDescription()); 103 | sendMessage(sender, " Version", firmware.getVersion()); 104 | 105 | sendMessage(sender, " Release date", firmware.getReleaseDate()); 106 | } 107 | 108 | private void printSensorsInfo(CommandSender sender, Sensors sensors) { 109 | double cpuTemperature = sensors.getCpuTemperature(); 110 | sendMessage(sender, "CPU Temp °C", String.valueOf(LagUtils.round(cpuTemperature))); 111 | sendMessage(sender, "Voltage", String.valueOf(LagUtils.round(sensors.getCpuVoltage()))); 112 | 113 | int[] fanSpeeds = sensors.getFanSpeeds(); 114 | sendMessage(sender, "Fan speed (rpm)", Arrays.toString(fanSpeeds)); 115 | } 116 | 117 | private void printDiskInfo(CommandSender sender, List diskStores) { 118 | //disk read write 119 | long diskReads = diskStores.stream().mapToLong(HWDiskStore::getReadBytes).sum(); 120 | long diskWrites = diskStores.stream().mapToLong(HWDiskStore::getWriteBytes).sum(); 121 | 122 | sendMessage(sender, "Disk read bytes", LagUtils.readableBytes(diskReads)); 123 | sendMessage(sender, "Disk write bytes", LagUtils.readableBytes(diskWrites)); 124 | 125 | sender.sendMessage(PRIMARY_COLOR + "Disks:"); 126 | for (HWDiskStore disk : diskStores) { 127 | String size = LagUtils.readableBytes(disk.getSize()); 128 | sendMessage(sender, " " + disk.getName(), disk.getModel() + ' ' + size); 129 | } 130 | } 131 | 132 | private void displayMounts(CommandSender sender, List fileStores) { 133 | sender.sendMessage(PRIMARY_COLOR + "Mounts:"); 134 | for (OSFileStore fileStore : fileStores) { 135 | printMountInfo(sender, fileStore); 136 | } 137 | } 138 | 139 | private void printMountInfo(CommandSender sender, OSFileStore fileStore) { 140 | String type = fileStore.getType(); 141 | String desc = fileStore.getDescription(); 142 | 143 | long totalSpaceBytes = fileStore.getTotalSpace(); 144 | String totalSpace = LagUtils.readableBytes(totalSpaceBytes); 145 | String usedSpace = LagUtils.readableBytes(totalSpaceBytes - fileStore.getUsableSpace()); 146 | 147 | String format = desc + ' ' + type + ' ' + usedSpace + '/' + totalSpace; 148 | sendMessage(sender, " " + fileStore.getMount(), format); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/main/java/com/github/games647/lagmonitor/command/NetworkCommand.java: -------------------------------------------------------------------------------- 1 | package com.github.games647.lagmonitor.command; 2 | 3 | import com.github.games647.lagmonitor.LagMonitor; 4 | import com.github.games647.lagmonitor.util.LagUtils; 5 | 6 | import java.util.Arrays; 7 | import java.util.Optional; 8 | 9 | import oshi.SystemInfo; 10 | import oshi.hardware.NetworkIF; 11 | import oshi.software.os.NetworkParams; 12 | 13 | import org.bukkit.command.Command; 14 | import org.bukkit.command.CommandSender; 15 | 16 | public class NetworkCommand extends LagCommand { 17 | 18 | public NetworkCommand(LagMonitor plugin) { 19 | super(plugin); 20 | } 21 | 22 | @Override 23 | public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { 24 | if (!canExecute(sender, command)) { 25 | return true; 26 | } 27 | 28 | Optional optInfo = plugin.getNativeData().getSystemInfo(); 29 | if (optInfo.isPresent()) { 30 | displayNetworkInfo(sender, optInfo.get()); 31 | } else { 32 | sender.sendMessage(NATIVE_NOT_FOUND); 33 | } 34 | 35 | return true; 36 | } 37 | 38 | private void displayNetworkInfo(CommandSender sender, SystemInfo systemInfo) { 39 | displayGlobalNetworkInfo(sender, systemInfo.getOperatingSystem().getNetworkParams()); 40 | for (NetworkIF networkInterface : systemInfo.getHardware().getNetworkIFs()) { 41 | displayInterfaceInfo(sender, networkInterface); 42 | } 43 | } 44 | 45 | private void displayGlobalNetworkInfo(CommandSender sender, NetworkParams networkParams) { 46 | sendMessage(sender, "Domain name", networkParams.getDomainName()); 47 | sendMessage(sender, "Host name", networkParams.getHostName()); 48 | sendMessage(sender, "Default IPv4 Gateway", networkParams.getIpv4DefaultGateway()); 49 | sendMessage(sender, "Default IPv6 Gateway", networkParams.getIpv6DefaultGateway()); 50 | sendMessage(sender, "DNS servers", Arrays.toString(networkParams.getDnsServers())); 51 | } 52 | 53 | private void displayInterfaceInfo(CommandSender sender, NetworkIF networkInterface) { 54 | sendMessage(sender, "Name", networkInterface.getName()); 55 | sendMessage(sender, " Display", networkInterface.getDisplayName()); 56 | sendMessage(sender, " MAC", networkInterface.getMacaddr()); 57 | sendMessage(sender, " MTU", String.valueOf(networkInterface.getMTU())); 58 | sendMessage(sender, " IPv4", Arrays.toString(networkInterface.getIPv4addr())); 59 | sendMessage(sender, " IPv6", Arrays.toString(networkInterface.getIPv6addr())); 60 | sendMessage(sender, " Speed", String.valueOf(networkInterface.getSpeed())); 61 | 62 | String receivedBytes = LagUtils.readableBytes(networkInterface.getBytesRecv()); 63 | String sentBytes = LagUtils.readableBytes(networkInterface.getBytesSent()); 64 | sendMessage(sender, " Received", receivedBytes); 65 | sendMessage(sender, " Sent", sentBytes); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/com/github/games647/lagmonitor/command/PaginationCommand.java: -------------------------------------------------------------------------------- 1 | package com.github.games647.lagmonitor.command; 2 | 3 | import com.github.games647.lagmonitor.LagMonitor; 4 | import com.github.games647.lagmonitor.Pages; 5 | import com.github.games647.lagmonitor.command.dump.DumpCommand; 6 | import com.google.common.primitives.Ints; 7 | 8 | import java.io.IOException; 9 | import java.nio.file.Files; 10 | import java.nio.file.Path; 11 | import java.util.Collections; 12 | import java.util.logging.Level; 13 | 14 | import net.md_5.bungee.api.chat.BaseComponent; 15 | 16 | import org.bukkit.ChatColor; 17 | import org.bukkit.command.Command; 18 | import org.bukkit.command.CommandSender; 19 | import org.bukkit.entity.Player; 20 | 21 | public class PaginationCommand extends DumpCommand { 22 | 23 | public PaginationCommand(LagMonitor plugin) { 24 | super(plugin, "pagination", "txt"); 25 | } 26 | 27 | @Override 28 | public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { 29 | if (!canExecute(sender, command)) { 30 | return true; 31 | } 32 | 33 | Pages pagination = plugin.getPageManager().getPagination(sender.getName()); 34 | if (pagination == null) { 35 | sendError(sender, "You have no pagination session"); 36 | return true; 37 | } 38 | 39 | if (args.length > 0) { 40 | String subCommand = args[0].toLowerCase(); 41 | switch (subCommand) { 42 | case "next": 43 | onNextPage(pagination, sender); 44 | break; 45 | case "prev": 46 | onPrevPage(pagination, sender); 47 | break; 48 | case "all": 49 | onShowAll(pagination, sender); 50 | break; 51 | case "save": 52 | onSave(pagination, sender); 53 | break; 54 | default: 55 | onPageNumber(subCommand, sender, pagination); 56 | } 57 | } else { 58 | sendError(sender, "Not enough arguments"); 59 | } 60 | 61 | return true; 62 | } 63 | 64 | private void onPageNumber(String subCommand, CommandSender sender, Pages pagination) { 65 | Integer page = Ints.tryParse(subCommand); 66 | if (page == null) { 67 | sendError(sender, "Unknown subcommand or not a valid page number"); 68 | } else { 69 | if (page < 1) { 70 | sendError(sender, "Page number too small"); 71 | return; 72 | } else if (page > pagination.getTotalPages(sender instanceof Player)) { 73 | sendError(sender, "Page number too high"); 74 | return; 75 | } 76 | 77 | pagination.send(sender, page); 78 | } 79 | } 80 | 81 | private void onNextPage(Pages pagination, CommandSender sender) { 82 | int lastPage = pagination.getLastSentPage(); 83 | if (lastPage >= pagination.getTotalPages(sender instanceof Player)) { 84 | sendError(sender,"You are already on the last page"); 85 | return; 86 | } 87 | 88 | pagination.send(sender, lastPage + 1); 89 | } 90 | 91 | private void onPrevPage(Pages pagination, CommandSender sender) { 92 | int lastPage = pagination.getLastSentPage(); 93 | if (lastPage <= 1) { 94 | sendError(sender,"You are already on the first page"); 95 | return; 96 | } 97 | 98 | pagination.send(sender, lastPage - 1); 99 | } 100 | 101 | private void onSave(Pages pagination, CommandSender sender) { 102 | StringBuilder lineBuilder = new StringBuilder(); 103 | for (BaseComponent[] line : pagination.getAllLines()) { 104 | for (BaseComponent component : line) { 105 | lineBuilder.append(component.toLegacyText()); 106 | } 107 | 108 | lineBuilder.append('\n'); 109 | } 110 | 111 | Path dumpFile = getNewDumpFile(); 112 | try { 113 | Files.write(dumpFile, Collections.singletonList(lineBuilder.toString())); 114 | sender.sendMessage(ChatColor.GRAY + "Dump created: " + dumpFile.getFileName()); 115 | } catch (IOException ex) { 116 | plugin.getLogger().log(Level.SEVERE, null, ex); 117 | } 118 | } 119 | 120 | private void onShowAll(Pages pagination, CommandSender sender) { 121 | if (sender instanceof Player) { 122 | Player player = (Player) sender; 123 | player.spigot().sendMessage(pagination.buildHeader(1, 1)); 124 | } else { 125 | BaseComponent[] header = pagination.buildHeader(1, 1); 126 | StringBuilder headerBuilder = new StringBuilder(); 127 | for (BaseComponent component : header) { 128 | headerBuilder.append(component.toLegacyText()); 129 | } 130 | 131 | sender.sendMessage(headerBuilder.toString()); 132 | } 133 | 134 | pagination.getAllLines().stream().map((line) -> { 135 | StringBuilder lineBuilder = new StringBuilder(); 136 | for (BaseComponent component : line) { 137 | lineBuilder.append(component.toLegacyText()); 138 | } 139 | 140 | return lineBuilder.toString(); 141 | }).forEach(sender::sendMessage); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/main/java/com/github/games647/lagmonitor/command/StackTraceCommand.java: -------------------------------------------------------------------------------- 1 | package com.github.games647.lagmonitor.command; 2 | 3 | import com.github.games647.lagmonitor.LagMonitor; 4 | import com.github.games647.lagmonitor.Pages; 5 | 6 | import java.lang.management.ManagementFactory; 7 | import java.lang.management.ThreadInfo; 8 | import java.lang.management.ThreadMXBean; 9 | import java.util.ArrayList; 10 | import java.util.Arrays; 11 | import java.util.List; 12 | import java.util.Map; 13 | import java.util.stream.Collectors; 14 | 15 | import net.md_5.bungee.api.ChatColor; 16 | import net.md_5.bungee.api.chat.BaseComponent; 17 | import net.md_5.bungee.api.chat.ComponentBuilder; 18 | 19 | import org.bukkit.command.Command; 20 | import org.bukkit.command.CommandSender; 21 | import org.bukkit.command.TabExecutor; 22 | 23 | public class StackTraceCommand extends LagCommand implements TabExecutor { 24 | 25 | private static final int MAX_DEPTH = 75; 26 | 27 | public StackTraceCommand(LagMonitor plugin) { 28 | super(plugin); 29 | } 30 | 31 | @Override 32 | public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { 33 | if (!canExecute(sender, command)) { 34 | return true; 35 | } 36 | 37 | if (args.length > 0) { 38 | String threadName = args[0]; 39 | 40 | Map allStackTraces = Thread.getAllStackTraces(); 41 | for (Map.Entry entry : allStackTraces.entrySet()) { 42 | Thread thread = entry.getKey(); 43 | if (thread.getName().equalsIgnoreCase(threadName)) { 44 | StackTraceElement[] stackTrace = entry.getValue(); 45 | printStackTrace(sender, stackTrace); 46 | return true; 47 | } 48 | } 49 | 50 | sendError(sender, "No thread with that name found"); 51 | } else { 52 | ThreadMXBean threadBean = ManagementFactory.getThreadMXBean(); 53 | ThreadInfo threadInfo = threadBean.getThreadInfo(Thread.currentThread().getId(), MAX_DEPTH); 54 | printStackTrace(sender, threadInfo.getStackTrace()); 55 | } 56 | 57 | return true; 58 | } 59 | 60 | private void printStackTrace(CommandSender sender, StackTraceElement[] stackTrace) { 61 | List lines = new ArrayList<>(); 62 | 63 | //begin from the top 64 | for (int i = stackTrace.length - 1; i >= 0; i--) { 65 | lines.add(formatTraceElement(stackTrace[i])); 66 | } 67 | 68 | Pages pagination = new Pages("Stacktrace", lines); 69 | pagination.send(sender); 70 | plugin.getPageManager().setPagination(sender.getName(), pagination); 71 | } 72 | 73 | private BaseComponent[] formatTraceElement(StackTraceElement traceElement) { 74 | String className = Pages.filterPackageNames(traceElement.getClassName()); 75 | String methodName = traceElement.getMethodName(); 76 | 77 | boolean nativeMethod = traceElement.isNativeMethod(); 78 | int lineNumber = traceElement.getLineNumber(); 79 | 80 | String line = Integer.toString(lineNumber); 81 | if (nativeMethod) { 82 | line = "Native"; 83 | } 84 | 85 | return new ComponentBuilder(className + '.') 86 | .color(PRIMARY_COLOR.asBungee()) 87 | .append(methodName + ':') 88 | .color(ChatColor.DARK_GREEN) 89 | .append(line) 90 | .color(ChatColor.DARK_PURPLE) 91 | .create(); 92 | } 93 | 94 | @Override 95 | public List onTabComplete(CommandSender sender, Command command, String alias, String[] args) { 96 | List result = new ArrayList<>(); 97 | 98 | StringBuilder builder = new StringBuilder(); 99 | for (String arg : args) { 100 | builder.append(arg).append(' '); 101 | } 102 | 103 | String requestName = builder.toString(); 104 | 105 | ThreadInfo[] threads = ManagementFactory.getThreadMXBean().dumpAllThreads(false, false); 106 | return Arrays.stream(threads) 107 | .map(ThreadInfo::getThreadName) 108 | .filter(name -> name.startsWith(requestName)) 109 | .sorted(String.CASE_INSENSITIVE_ORDER) 110 | .collect(Collectors.toList()); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/main/java/com/github/games647/lagmonitor/command/VmCommand.java: -------------------------------------------------------------------------------- 1 | package com.github.games647.lagmonitor.command; 2 | 3 | import com.github.games647.lagmonitor.LagMonitor; 4 | import com.github.games647.lagmonitor.util.JavaVersion; 5 | 6 | import java.lang.management.ClassLoadingMXBean; 7 | import java.lang.management.CompilationMXBean; 8 | import java.lang.management.GarbageCollectorMXBean; 9 | import java.lang.management.ManagementFactory; 10 | import java.lang.management.RuntimeMXBean; 11 | 12 | import net.md_5.bungee.api.ChatColor; 13 | import net.md_5.bungee.api.chat.BaseComponent; 14 | import net.md_5.bungee.api.chat.ComponentBuilder; 15 | import net.md_5.bungee.api.chat.HoverEvent; 16 | import net.md_5.bungee.api.chat.HoverEvent.Action; 17 | 18 | import org.bukkit.command.Command; 19 | import org.bukkit.command.CommandSender; 20 | 21 | public class VmCommand extends LagCommand { 22 | 23 | public VmCommand(LagMonitor plugin) { 24 | super(plugin); 25 | } 26 | 27 | @Override 28 | public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { 29 | if (!canExecute(sender, command)) { 30 | return true; 31 | } 32 | 33 | //java version info 34 | displayJavaVersion(sender); 35 | 36 | //java paths 37 | sendMessage(sender, "Java lib", System.getProperty("sun.boot.library.path", "Unknown")); 38 | sendMessage(sender, "Java home", System.getProperty("java.home", "Unknown")); 39 | sendMessage(sender, "Temp path", System.getProperty("java.io.tmpdir", "Unknown")); 40 | 41 | displayRuntimeInfo(sender, ManagementFactory.getRuntimeMXBean()); 42 | displayCompilationInfo(sender, ManagementFactory.getCompilationMXBean()); 43 | displayClassLoading(sender, ManagementFactory.getClassLoadingMXBean()); 44 | 45 | //garbage collector 46 | for (GarbageCollectorMXBean collector : ManagementFactory.getGarbageCollectorMXBeans()) { 47 | displayCollectorStats(sender, collector); 48 | } 49 | 50 | return true; 51 | } 52 | 53 | private void displayCompilationInfo(CommandSender sender, CompilationMXBean compilationBean) { 54 | sendMessage(sender, "Compiler name", compilationBean.getName()); 55 | sendMessage(sender, "Compilation time (ms)", String.valueOf(compilationBean.getTotalCompilationTime())); 56 | } 57 | 58 | private void displayRuntimeInfo(CommandSender sender, RuntimeMXBean runtimeBean) { 59 | //vm 60 | sendMessage(sender, "Java VM", runtimeBean.getVmName() + ' ' + runtimeBean.getVmVersion()); 61 | sendMessage(sender, "Java vendor", runtimeBean.getVmVendor()); 62 | 63 | //vm specification 64 | sendMessage(sender, "Spec name", runtimeBean.getSpecName()); 65 | sendMessage(sender, "Spec vendor", runtimeBean.getSpecVendor()); 66 | sendMessage(sender, "Spec version", runtimeBean.getSpecVersion()); 67 | } 68 | 69 | private void displayCollectorStats(CommandSender sender, GarbageCollectorMXBean collector) { 70 | sendMessage(sender, "Garbage collector", collector.getName()); 71 | sendMessage(sender, " Count", String.valueOf(collector.getCollectionCount())); 72 | sendMessage(sender, " Time (ms)", String.valueOf(collector.getCollectionTime())); 73 | } 74 | 75 | private void displayClassLoading(CommandSender sender, ClassLoadingMXBean classBean) { 76 | sendMessage(sender, "Loaded classes", String.valueOf(classBean.getLoadedClassCount())); 77 | sendMessage(sender, "Total loaded", String.valueOf(classBean.getTotalLoadedClassCount())); 78 | sendMessage(sender, "Unloaded classes", String.valueOf(classBean.getUnloadedClassCount())); 79 | } 80 | 81 | private void displayJavaVersion(CommandSender sender) { 82 | JavaVersion currentVersion = JavaVersion.detect(); 83 | LagCommand.send(sender, formatJavaVersion(currentVersion)); 84 | 85 | sendMessage(sender, "Java release date", System.getProperty("java.version.date", "n/a")); 86 | sendMessage(sender, "Class version", System.getProperty("java.class.version")); 87 | } 88 | 89 | private BaseComponent[] formatJavaVersion(JavaVersion version) { 90 | ComponentBuilder builder = new ComponentBuilder("Java version: ").color(PRIMARY_COLOR.asBungee()) 91 | .append(version.getRaw()).color(SECONDARY_COLOR.asBungee()); 92 | if (version.isOutdated()) { 93 | builder = builder.append(" (").color(ChatColor.WHITE) 94 | .append("Outdated").color(ChatColor.DARK_RED) 95 | .event(new HoverEvent(Action.SHOW_TEXT, 96 | new ComponentBuilder("You're running an outdated Java version. \n" 97 | + "Java 9 and 10 are already released. \n" 98 | + "Newer versions could improve the performance or include bug or security fixes.") 99 | .color(ChatColor.DARK_AQUA).create())) 100 | .append(")").color(ChatColor.WHITE); 101 | } 102 | 103 | return builder.create(); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/main/java/com/github/games647/lagmonitor/command/dump/DumpCommand.java: -------------------------------------------------------------------------------- 1 | package com.github.games647.lagmonitor.command.dump; 2 | 3 | import com.github.games647.lagmonitor.LagMonitor; 4 | import com.github.games647.lagmonitor.command.LagCommand; 5 | 6 | import java.lang.management.ManagementFactory; 7 | import java.nio.file.Path; 8 | import java.time.LocalDateTime; 9 | import java.time.format.DateTimeFormatter; 10 | 11 | import javax.management.InstanceNotFoundException; 12 | import javax.management.MBeanException; 13 | import javax.management.MBeanServer; 14 | import javax.management.MalformedObjectNameException; 15 | import javax.management.ObjectName; 16 | import javax.management.ReflectionException; 17 | 18 | public abstract class DumpCommand extends LagCommand { 19 | 20 | //https://docs.oracle.com/javase/10/docs/jre/api/management/extension/com/sun/management/DiagnosticCommandMBean.html 21 | protected static final String DIAGNOSTIC_BEAN = "com.sun.management:type=DiagnosticCommand"; 22 | protected static final String NOT_ORACLE_MSG = "You are not using Oracle JVM. OpenJDK hasn't implemented it yet"; 23 | 24 | private final String filePrefix; 25 | private final String fileExt; 26 | 27 | private final DateTimeFormatter dateFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd-HH-mm-ss"); 28 | 29 | public DumpCommand(LagMonitor plugin, String filePrefix, String fileExt) { 30 | super(plugin); 31 | 32 | this.filePrefix = filePrefix; 33 | this.fileExt = '.' + fileExt; 34 | } 35 | 36 | public Path getNewDumpFile() { 37 | String timeSuffix = '-' + LocalDateTime.now().format(dateFormat); 38 | Path folder = plugin.getDataFolder().toPath(); 39 | return folder.resolve(filePrefix + '-' + timeSuffix + fileExt); 40 | } 41 | 42 | public Object invokeBeanCommand(String beanName, String command, Object[] args, String[] signature) 43 | throws MalformedObjectNameException, MBeanException, InstanceNotFoundException, ReflectionException { 44 | MBeanServer beanServer = ManagementFactory.getPlatformMBeanServer(); 45 | ObjectName beanObject = ObjectName.getInstance(beanName); 46 | 47 | return beanServer.invoke(beanObject, command, args, signature); 48 | } 49 | 50 | public String invokeDiagnosticCommand(String command, String... args) 51 | throws MalformedObjectNameException, ReflectionException, MBeanException, InstanceNotFoundException { 52 | return (String) invokeBeanCommand(DIAGNOSTIC_BEAN, command, 53 | new Object[]{args}, new String[]{String[].class.getName()}); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/com/github/games647/lagmonitor/command/dump/FlightCommand.java: -------------------------------------------------------------------------------- 1 | package com.github.games647.lagmonitor.command.dump; 2 | 3 | import com.github.games647.lagmonitor.LagMonitor; 4 | 5 | import java.lang.management.ManagementFactory; 6 | import java.nio.file.Path; 7 | import java.util.Arrays; 8 | import java.util.logging.Level; 9 | 10 | import javax.management.InstanceNotFoundException; 11 | import javax.management.JMException; 12 | import javax.management.MBeanException; 13 | import javax.management.MBeanFeatureInfo; 14 | import javax.management.MBeanInfo; 15 | import javax.management.MBeanServer; 16 | import javax.management.MalformedObjectNameException; 17 | import javax.management.ObjectName; 18 | import javax.management.ReflectionException; 19 | 20 | import org.bukkit.command.Command; 21 | import org.bukkit.command.CommandSender; 22 | 23 | public class FlightCommand extends DumpCommand { 24 | 25 | private static final String START_COMMAND = "jfrStart"; 26 | private static final String STOP_COMMAND = "jfrStop"; 27 | private static final String DUMP_COMMAND = "jfrDump"; 28 | 29 | private static final String SETTINGS_FILE = "default.jfc"; 30 | 31 | private final String settingsPath; 32 | private final String recordingName; 33 | 34 | private final boolean isSupported; 35 | 36 | public FlightCommand(LagMonitor plugin) { 37 | super(plugin, "flight_recorder", "jfr"); 38 | 39 | this.recordingName = plugin.getName() + "-Record"; 40 | this.settingsPath = plugin.getDataFolder().toPath().resolve(SETTINGS_FILE).toAbsolutePath().toString(); 41 | 42 | isSupported = areFlightMethodsAvailable(); 43 | } 44 | 45 | private boolean areFlightMethodsAvailable() { 46 | MBeanServer beanServer = ManagementFactory.getPlatformMBeanServer(); 47 | try { 48 | ObjectName objectName = ObjectName.getInstance(DIAGNOSTIC_BEAN); 49 | MBeanInfo beanInfo = beanServer.getMBeanInfo(objectName); 50 | return Arrays.stream(beanInfo.getOperations()) 51 | .map(MBeanFeatureInfo::getName) 52 | .anyMatch(op -> op.contains("jfr")); 53 | } catch (JMException instanceNotFoundEx) { 54 | return false; 55 | } 56 | } 57 | 58 | @Override 59 | public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { 60 | if (!canExecute(sender, command)) { 61 | return true; 62 | } 63 | 64 | if (!isSupported) { 65 | sendError(sender, NOT_ORACLE_MSG); 66 | return true; 67 | } 68 | 69 | try { 70 | if (args.length > 0) { 71 | String subCommand = args[0].toLowerCase(); 72 | switch (subCommand) { 73 | case "start": 74 | onStartCommand(sender); 75 | break; 76 | case "stop": 77 | onStopCommand(sender); 78 | break; 79 | case "dump": 80 | onDumpCommand(sender); 81 | break; 82 | default: 83 | sendError(sender, "Unknown subcommand"); 84 | } 85 | } else { 86 | sendError(sender, "Not enough arguments"); 87 | } 88 | } catch (InstanceNotFoundException notFoundEx) { 89 | sendError(sender, NOT_ORACLE_MSG); 90 | } catch (Exception ex) { 91 | plugin.getLogger().log(Level.SEVERE, null, ex); 92 | sendError(sender, "An exception occurred. Please check the server log"); 93 | } 94 | 95 | return true; 96 | } 97 | 98 | private void onStartCommand(CommandSender sender) 99 | throws MalformedObjectNameException, ReflectionException, MBeanException, InstanceNotFoundException { 100 | String reply = invokeDiagnosticCommand(START_COMMAND, "settings=" + settingsPath, "name=" + recordingName); 101 | sender.sendMessage(reply); 102 | } 103 | 104 | private void onStopCommand(CommandSender sender) 105 | throws MalformedObjectNameException, ReflectionException, MBeanException, InstanceNotFoundException { 106 | String reply = invokeDiagnosticCommand(STOP_COMMAND, "name=" + recordingName); 107 | sender.sendMessage(reply); 108 | } 109 | 110 | private void onDumpCommand(CommandSender sender) 111 | throws MalformedObjectNameException, ReflectionException, MBeanException, InstanceNotFoundException { 112 | Path dumpFile = getNewDumpFile(); 113 | String reply = invokeDiagnosticCommand(DUMP_COMMAND 114 | , "filename=" + dumpFile.toAbsolutePath(), "name=" + recordingName); 115 | 116 | sender.sendMessage(reply); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/main/java/com/github/games647/lagmonitor/command/dump/HeapCommand.java: -------------------------------------------------------------------------------- 1 | package com.github.games647.lagmonitor.command.dump; 2 | 3 | import com.github.games647.lagmonitor.LagMonitor; 4 | import com.github.games647.lagmonitor.Pages; 5 | import com.sun.management.HotSpotDiagnosticMXBean; 6 | 7 | import java.lang.management.ManagementFactory; 8 | import java.nio.file.Path; 9 | import java.util.ArrayList; 10 | import java.util.List; 11 | import java.util.logging.Level; 12 | 13 | import javax.management.InstanceNotFoundException; 14 | 15 | import net.md_5.bungee.api.ChatColor; 16 | import net.md_5.bungee.api.chat.BaseComponent; 17 | import net.md_5.bungee.api.chat.ComponentBuilder; 18 | 19 | import org.bukkit.command.Command; 20 | import org.bukkit.command.CommandSender; 21 | 22 | public class HeapCommand extends DumpCommand { 23 | 24 | private static final String HEAP_COMMAND = "gcClassHistogram"; 25 | private static final boolean DUMP_DEAD_OBJECTS = false; 26 | 27 | private static final String[] EMPTY_STRING = {}; 28 | 29 | public HeapCommand(LagMonitor plugin) { 30 | super(plugin, "heap", "hprof"); 31 | } 32 | 33 | @Override 34 | public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { 35 | if (!canExecute(sender, command)) { 36 | return true; 37 | } 38 | 39 | if (args.length > 0) { 40 | String subCommand = args[0]; 41 | if ("dump".equalsIgnoreCase(subCommand)) { 42 | onDump(sender); 43 | } else { 44 | sendError(sender, "Unknown subcommand"); 45 | } 46 | 47 | return true; 48 | } 49 | 50 | List paginatedLines = new ArrayList<>(); 51 | try { 52 | String reply = invokeDiagnosticCommand(HEAP_COMMAND, EMPTY_STRING); 53 | for (String line : reply.split("\n")) { 54 | paginatedLines.add(new ComponentBuilder(line).create()); 55 | } 56 | 57 | Pages pagination = new Pages("Heap", paginatedLines); 58 | pagination.send(sender); 59 | plugin.getPageManager().setPagination(sender.getName(), pagination); 60 | } catch (InstanceNotFoundException instanceNotFoundException) { 61 | sendError(sender, NOT_ORACLE_MSG); 62 | } catch (Exception ex) { 63 | plugin.getLogger().log(Level.SEVERE, null, ex); 64 | sendError(sender, "An exception occurred. Please check the server log"); 65 | } 66 | 67 | return true; 68 | } 69 | 70 | private void onDump(CommandSender sender) { 71 | try { 72 | //test if this class is available 73 | Class.forName("com.sun.management.HotSpotDiagnosticMXBean"); 74 | 75 | //can be useful for dumping heaps in binary format 76 | HotSpotDiagnosticMXBean hostSpot = ManagementFactory.getPlatformMXBean(HotSpotDiagnosticMXBean.class); 77 | 78 | Path dumpFile = getNewDumpFile(); 79 | hostSpot.dumpHeap(dumpFile.toAbsolutePath().toString(), DUMP_DEAD_OBJECTS); 80 | 81 | sender.sendMessage(ChatColor.GRAY + "Dump created: " + dumpFile.getFileName()); 82 | sender.sendMessage(ChatColor.GRAY + "You can analyse it using VisualVM"); 83 | } catch (ClassNotFoundException notFoundEx) { 84 | sendError(sender, NOT_ORACLE_MSG); 85 | } catch (Exception ex) { 86 | plugin.getLogger().log(Level.SEVERE, null, ex); 87 | sendError(sender, "An exception occurred. Please check the server log"); 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/main/java/com/github/games647/lagmonitor/command/dump/ThreadCommand.java: -------------------------------------------------------------------------------- 1 | package com.github.games647.lagmonitor.command.dump; 2 | 3 | import com.github.games647.lagmonitor.LagMonitor; 4 | import com.github.games647.lagmonitor.Pages; 5 | 6 | import java.nio.file.Files; 7 | import java.nio.file.Path; 8 | import java.util.ArrayList; 9 | import java.util.Collections; 10 | import java.util.List; 11 | import java.util.Map; 12 | import java.util.logging.Level; 13 | 14 | import javax.management.InstanceNotFoundException; 15 | 16 | import net.md_5.bungee.api.ChatColor; 17 | import net.md_5.bungee.api.chat.BaseComponent; 18 | import net.md_5.bungee.api.chat.ClickEvent; 19 | import net.md_5.bungee.api.chat.ComponentBuilder; 20 | import net.md_5.bungee.api.chat.HoverEvent; 21 | 22 | import org.bukkit.command.Command; 23 | import org.bukkit.command.CommandSender; 24 | 25 | public class ThreadCommand extends DumpCommand { 26 | 27 | private static final String DUMP_COMMAND = "threadPrint"; 28 | private static final String[] EMPTY_STRING_ARRAY = {}; 29 | 30 | public ThreadCommand(LagMonitor plugin) { 31 | super(plugin, "thread", "tdump"); 32 | } 33 | 34 | @Override 35 | public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { 36 | if (!canExecute(sender, command)) { 37 | return true; 38 | } 39 | 40 | if (args.length > 0) { 41 | String subCommand = args[0]; 42 | if ("dump".equalsIgnoreCase(subCommand)) { 43 | onDump(sender); 44 | } else { 45 | sender.sendMessage(label); 46 | } 47 | 48 | return true; 49 | } 50 | 51 | List lines = new ArrayList<>(); 52 | 53 | Map allStackTraces = Thread.getAllStackTraces(); 54 | for (Thread thread : allStackTraces.keySet()) { 55 | if (thread.getContextClassLoader() == null) { 56 | //ignore java system threads like reference handler 57 | continue; 58 | } 59 | 60 | BaseComponent[] components = new ComponentBuilder("ID-" + thread.getId() + ": ") 61 | .color(PRIMARY_COLOR.asBungee()) 62 | .event(new ClickEvent(ClickEvent.Action.RUN_COMMAND 63 | , "/stacktrace " + thread.getName())) 64 | .event(new HoverEvent(HoverEvent.Action.SHOW_TEXT 65 | , new ComponentBuilder("Show the stacktrace").create())) 66 | .append(thread.getName() + ' ') 67 | .color(ChatColor.GOLD) 68 | .append(thread.getState().toString()) 69 | .color(SECONDARY_COLOR.asBungee()) 70 | .create(); 71 | lines.add(components); 72 | } 73 | 74 | Pages pagination = new Pages("Threads", lines); 75 | pagination.send(sender); 76 | plugin.getPageManager().setPagination(sender.getName(), pagination); 77 | return true; 78 | } 79 | 80 | private void onDump(CommandSender sender) { 81 | try { 82 | String result = invokeDiagnosticCommand(DUMP_COMMAND, EMPTY_STRING_ARRAY); 83 | 84 | Path dumpFile = getNewDumpFile(); 85 | Files.write(dumpFile, Collections.singletonList(result)); 86 | 87 | sender.sendMessage(ChatColor.GRAY + "Dump created: " + dumpFile.getFileName()); 88 | sender.sendMessage(ChatColor.GRAY + "You can analyse it using VisualVM"); 89 | } catch (InstanceNotFoundException instanceNotFoundException) { 90 | sendError(sender, NOT_ORACLE_MSG); 91 | } catch (Exception ex) { 92 | plugin.getLogger().log(Level.SEVERE, null, ex); 93 | sendError(sender, "An exception occurred. Please check the server log"); 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/main/java/com/github/games647/lagmonitor/command/minecraft/PingCommand.java: -------------------------------------------------------------------------------- 1 | package com.github.games647.lagmonitor.command.minecraft; 2 | 3 | import com.github.games647.lagmonitor.LagMonitor; 4 | import com.github.games647.lagmonitor.command.LagCommand; 5 | import com.github.games647.lagmonitor.util.LagUtils; 6 | import com.github.games647.lagmonitor.util.RollingOverHistory; 7 | 8 | import org.bukkit.Bukkit; 9 | import org.bukkit.ChatColor; 10 | import org.bukkit.command.Command; 11 | import org.bukkit.command.CommandSender; 12 | import org.bukkit.entity.Player; 13 | 14 | public class PingCommand extends LagCommand { 15 | 16 | public PingCommand(LagMonitor plugin) { 17 | super(plugin); 18 | } 19 | 20 | @Override 21 | public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { 22 | if (!canExecute(sender, command)) { 23 | return true; 24 | } 25 | 26 | if (args.length > 0) { 27 | displayPingOther(sender, command, args[0]); 28 | } else if (sender instanceof Player) { 29 | displayPingSelf(sender); 30 | } else { 31 | sendError(sender, "You have to be in game in order to see your own ping"); 32 | } 33 | 34 | return true; 35 | } 36 | 37 | private void displayPingSelf(CommandSender sender) { 38 | RollingOverHistory history = plugin.getPingManager().map(m -> m.getHistory(sender.getName())).orElse(null); 39 | if (history == null) { 40 | sendError(sender, "Sorry there is currently no data available"); 41 | return; 42 | } 43 | 44 | int lastPing = (int) history.getLastSample(); 45 | sender.sendMessage(PRIMARY_COLOR + "Your ping is: " + ChatColor.DARK_GREEN + lastPing + "ms"); 46 | 47 | float pingAverage = (float) (Math.round(history.getAverage() * 100.0) / 100.0); 48 | sender.sendMessage(PRIMARY_COLOR + "Average: " + ChatColor.DARK_GREEN + pingAverage + "ms"); 49 | } 50 | 51 | private void displayPingOther(CommandSender sender, Command command, String playerName) { 52 | if (sender.hasPermission(command.getPermission() + ".other")) { 53 | RollingOverHistory history = plugin.getPingManager().map(m -> m.getHistory(sender.getName())).orElse(null); 54 | if (history == null || !canSee(sender, playerName)) { 55 | sendError(sender, "No data for that player " + playerName); 56 | return; 57 | } 58 | 59 | int lastPing = (int) history.getLastSample(); 60 | 61 | sender.sendMessage(ChatColor.WHITE + playerName + PRIMARY_COLOR + "'s ping is: " 62 | + ChatColor.DARK_GREEN + lastPing + "ms"); 63 | 64 | float pingAverage = LagUtils.round(history.getAverage()); 65 | sender.sendMessage(PRIMARY_COLOR + "Average: " + ChatColor.DARK_GREEN + pingAverage + "ms"); 66 | } else { 67 | sendError(sender, "You don't have enough permission"); 68 | } 69 | } 70 | 71 | private boolean canSee(CommandSender sender, String playerName) { 72 | if (sender instanceof Player) { 73 | return ((Player) sender).canSee(Bukkit.getPlayerExact(playerName)); 74 | } 75 | 76 | return true; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/main/java/com/github/games647/lagmonitor/command/minecraft/TPSCommand.java: -------------------------------------------------------------------------------- 1 | package com.github.games647.lagmonitor.command.minecraft; 2 | 3 | import com.github.games647.lagmonitor.LagMonitor; 4 | import com.github.games647.lagmonitor.command.LagCommand; 5 | import com.github.games647.lagmonitor.task.TPSHistoryTask; 6 | import com.google.common.collect.Lists; 7 | 8 | import java.text.DecimalFormat; 9 | import java.util.List; 10 | import java.util.stream.IntStream; 11 | 12 | import org.bukkit.ChatColor; 13 | import org.bukkit.command.Command; 14 | import org.bukkit.command.CommandSender; 15 | import org.bukkit.entity.Player; 16 | import org.bukkit.util.ChatPaginator; 17 | 18 | public class TPSCommand extends LagCommand { 19 | 20 | private static final ChatColor PRIMARY_COLOR = ChatColor.DARK_AQUA; 21 | private static final ChatColor SECONDARY_COLOR = ChatColor.GRAY; 22 | 23 | private static final char EMPTY_CHAR = ' '; 24 | private static final char GRAPH_CHAR = '+'; 25 | 26 | private static final char PLAYER_EMPTY_CHAR = '▂'; 27 | private static final char PLAYER_GRAPH_CHAR = '▇'; 28 | 29 | private static final int GRAPH_WIDTH = 60 / 2; 30 | private static final int GRAPH_LINES = ChatPaginator.CLOSED_CHAT_PAGE_HEIGHT - 3; 31 | 32 | public TPSCommand(LagMonitor plugin) { 33 | super(plugin); 34 | } 35 | 36 | @Override 37 | public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { 38 | if (!canExecute(sender, command)) { 39 | return true; 40 | } 41 | 42 | List graphLines = Lists.newArrayListWithExpectedSize(GRAPH_LINES); 43 | IntStream.rangeClosed(1, GRAPH_LINES) 44 | .map(i -> GRAPH_WIDTH * 2) 45 | .mapToObj(StringBuilder::new) 46 | .forEach(graphLines::add); 47 | 48 | TPSHistoryTask tpsHistoryTask = plugin.getTpsHistoryTask(); 49 | 50 | boolean console = true; 51 | if (sender instanceof Player) { 52 | console = false; 53 | } 54 | 55 | float[] lastSeconds = tpsHistoryTask.getMinuteSample().getSamples(); 56 | int position = tpsHistoryTask.getMinuteSample().getCurrentPosition(); 57 | buildGraph(lastSeconds, position, graphLines, console); 58 | graphLines.stream().map(Object::toString).forEach(sender::sendMessage); 59 | 60 | printAverageHistory(tpsHistoryTask, sender); 61 | sender.sendMessage(PRIMARY_COLOR + "Current TPS: " + tpsHistoryTask.getLastSample()); 62 | return true; 63 | } 64 | 65 | private void printAverageHistory(TPSHistoryTask tpsHistoryTask, CommandSender sender) { 66 | float minuteAverage = tpsHistoryTask.getMinuteSample().getAverage(); 67 | float quarterAverage = tpsHistoryTask.getQuarterSample().getAverage(); 68 | float halfHourAverage = tpsHistoryTask.getHalfHourSample().getAverage(); 69 | 70 | DecimalFormat formatter = new DecimalFormat("###.##"); 71 | sender.sendMessage(PRIMARY_COLOR + "Last Samples (1m, 15m, 30m): " + SECONDARY_COLOR 72 | + formatter.format(minuteAverage) 73 | + ' ' + formatter.format(quarterAverage) 74 | + ' ' + formatter.format(halfHourAverage)); 75 | } 76 | 77 | private void buildGraph(float[] lastSeconds, int lastPos, List graphLines, boolean console) { 78 | int index = lastPos; 79 | //in x-direction 80 | for (int xPos = 1; xPos < GRAPH_WIDTH; xPos++) { 81 | index++; 82 | if (index == lastSeconds.length) { 83 | index = 0; 84 | } 85 | 86 | float sampleSecond = lastSeconds[index]; 87 | buildLine(sampleSecond, graphLines, console); 88 | } 89 | } 90 | 91 | private void buildLine(float sampleSecond, List graphLines, boolean console) { 92 | ChatColor color = ChatColor.DARK_RED; 93 | int lines = 0; 94 | if (sampleSecond > 19.5F) { 95 | lines = GRAPH_LINES; 96 | color = ChatColor.DARK_GREEN; 97 | } else if (sampleSecond > 18.0F) { 98 | lines = GRAPH_LINES - 1; 99 | color = ChatColor.GREEN; 100 | } else if (sampleSecond > 17.0F) { 101 | lines = GRAPH_LINES - 2; 102 | color = ChatColor.YELLOW; 103 | } else if (sampleSecond > 15.0F) { 104 | lines = GRAPH_LINES - 3; 105 | color = ChatColor.GOLD; 106 | } else if (sampleSecond > 12.0F) { 107 | lines = GRAPH_LINES - 4; 108 | color = ChatColor.RED; 109 | } 110 | 111 | //in y-direction in reverse order 112 | for (int line = GRAPH_LINES - 1; line >= 0; line--) { 113 | if (lines == 0) { 114 | graphLines.get(line).append(ChatColor.WHITE).append(console ? EMPTY_CHAR : PLAYER_EMPTY_CHAR); 115 | continue; 116 | } 117 | 118 | lines--; 119 | graphLines.get(line).append(color).append(console ? GRAPH_CHAR : PLAYER_GRAPH_CHAR); 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/main/java/com/github/games647/lagmonitor/command/minecraft/TasksCommand.java: -------------------------------------------------------------------------------- 1 | package com.github.games647.lagmonitor.command.minecraft; 2 | 3 | import com.github.games647.lagmonitor.LagMonitor; 4 | import com.github.games647.lagmonitor.Pages; 5 | import com.github.games647.lagmonitor.command.LagCommand; 6 | import com.github.games647.lagmonitor.traffic.Reflection; 7 | 8 | import java.lang.invoke.MethodHandle; 9 | import java.lang.invoke.MethodHandles; 10 | import java.util.ArrayList; 11 | import java.util.List; 12 | 13 | import net.md_5.bungee.api.chat.BaseComponent; 14 | import net.md_5.bungee.api.chat.ComponentBuilder; 15 | 16 | import org.bukkit.Bukkit; 17 | import org.bukkit.command.Command; 18 | import org.bukkit.command.CommandSender; 19 | import org.bukkit.plugin.Plugin; 20 | import org.bukkit.scheduler.BukkitTask; 21 | 22 | public class TasksCommand extends LagCommand { 23 | 24 | private static final MethodHandle taskHandle; 25 | 26 | static { 27 | Class taskClass = Reflection.getCraftBukkitClass("scheduler.CraftTask"); 28 | 29 | MethodHandle localHandle = null; 30 | try { 31 | localHandle = MethodHandles.publicLookup().findGetter(taskClass, "task", Runnable.class); 32 | } catch (NoSuchFieldException | IllegalAccessException noSuchFieldEx) { 33 | //ignore 34 | } 35 | 36 | taskHandle = localHandle; 37 | } 38 | 39 | public TasksCommand(LagMonitor plugin) { 40 | super(plugin); 41 | } 42 | 43 | @Override 44 | public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { 45 | if (!canExecute(sender, command)) { 46 | return true; 47 | } 48 | 49 | List lines = new ArrayList<>(); 50 | 51 | List pendingTasks = Bukkit.getScheduler().getPendingTasks(); 52 | for (BukkitTask pendingTask : pendingTasks) { 53 | lines.add(formatTask(pendingTask)); 54 | 55 | Class runnableClass = getRunnableClass(pendingTask); 56 | if (runnableClass != null) { 57 | lines.add(new ComponentBuilder(" Task: ") 58 | .color(PRIMARY_COLOR.asBungee()) 59 | .append(runnableClass.getSimpleName()) 60 | .color(SECONDARY_COLOR.asBungee()) 61 | .create()); 62 | } 63 | } 64 | 65 | Pages pagination = new Pages("Stacktrace", lines); 66 | pagination.send(sender); 67 | plugin.getPageManager().setPagination(sender.getName(), pagination); 68 | return true; 69 | } 70 | 71 | private BaseComponent[] formatTask(BukkitTask pendingTask) { 72 | Plugin owner = pendingTask.getOwner(); 73 | int taskId = pendingTask.getTaskId(); 74 | boolean sync = pendingTask.isSync(); 75 | 76 | String id = Integer.toString(taskId); 77 | if (sync) { 78 | id += "-Sync"; 79 | } else if (Bukkit.getScheduler().isCurrentlyRunning(taskId)) { 80 | id += "-Running"; 81 | } 82 | 83 | return new ComponentBuilder(owner.getName()) 84 | .color(PRIMARY_COLOR.asBungee()) 85 | .append('-' + id) 86 | .color(SECONDARY_COLOR.asBungee()) 87 | .create(); 88 | } 89 | 90 | private Class getRunnableClass(BukkitTask task) { 91 | try { 92 | return taskHandle.invokeExact(task).getClass(); 93 | } catch (Exception ex) { 94 | //ignore 95 | } catch (Throwable throwable) { 96 | throw (Error) throwable; 97 | } 98 | 99 | return null; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/main/java/com/github/games647/lagmonitor/command/timing/Timing.java: -------------------------------------------------------------------------------- 1 | package com.github.games647.lagmonitor.command.timing; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | import java.util.Objects; 6 | 7 | public class Timing implements Comparable { 8 | 9 | private final String category; 10 | 11 | private long totalTime; 12 | private long totalCount; 13 | 14 | private Map subcategories; 15 | 16 | public Timing(String category) { 17 | this.category = category; 18 | } 19 | 20 | public Timing(String category, long totalTime, long count) { 21 | this.category = category; 22 | this.totalTime = totalTime; 23 | this.totalCount = count; 24 | } 25 | 26 | public String getCategoryName() { 27 | return category; 28 | } 29 | 30 | public long getTotalTime() { 31 | return totalTime; 32 | } 33 | 34 | public void addTotal(long total) { 35 | this.totalTime += total; 36 | } 37 | 38 | public long getTotalCount() { 39 | return totalCount; 40 | } 41 | 42 | public void addCount(long count) { 43 | this.totalCount += count; 44 | } 45 | 46 | public double calculateAverage() { 47 | if (totalCount == 0) { 48 | return 0; 49 | } 50 | 51 | return (double) totalTime / totalCount; 52 | } 53 | 54 | public Map getSubCategories() { 55 | return subcategories; 56 | } 57 | 58 | public void addSubcategory(String name, long totalTime, long count) { 59 | if (subcategories == null) { 60 | //lazy creating 61 | subcategories = new HashMap<>(); 62 | } 63 | 64 | Timing timing = subcategories.computeIfAbsent(name, key -> new Timing(key, totalTime, count)); 65 | timing.addTotal(totalTime); 66 | timing.addCount(totalTime); 67 | } 68 | 69 | @Override 70 | public boolean equals(Object o) { 71 | if (this == o) return true; 72 | if (o == null || getClass() != o.getClass()) return false; 73 | Timing timing = (Timing) o; 74 | return totalTime == timing.totalTime && 75 | totalCount == timing.totalCount && 76 | Objects.equals(category, timing.category) && 77 | Objects.equals(subcategories, timing.subcategories); 78 | } 79 | 80 | @Override 81 | public int hashCode() { 82 | return Objects.hash(category, totalTime, totalCount, subcategories); 83 | } 84 | 85 | @Override 86 | public int compareTo(Timing other) { 87 | return Long.compare(totalTime, other.totalTime); 88 | } 89 | 90 | @Override 91 | public String toString() { 92 | return this.getClass().getSimpleName() + '{' + 93 | "category='" + category + '\'' + 94 | ", totalTime=" + totalTime + 95 | ", totalCount=" + totalCount + 96 | ", subcategories=" + subcategories + 97 | '}'; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/main/java/com/github/games647/lagmonitor/command/timing/TimingCommand.java: -------------------------------------------------------------------------------- 1 | package com.github.games647.lagmonitor.command.timing; 2 | 3 | import com.github.games647.lagmonitor.LagMonitor; 4 | import com.github.games647.lagmonitor.command.LagCommand; 5 | 6 | import net.md_5.bungee.api.ChatColor; 7 | 8 | import org.bukkit.command.Command; 9 | import org.bukkit.command.CommandSender; 10 | 11 | public abstract class TimingCommand extends LagCommand { 12 | 13 | public TimingCommand(LagMonitor plugin) { 14 | super(plugin); 15 | } 16 | 17 | @Override 18 | public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { 19 | if (!canExecute(sender, command)) { 20 | return true; 21 | } 22 | 23 | if (!isTimingsEnabled()) { 24 | sendError(sender,"The server deactivated timing reports"); 25 | sendError(sender,"Go to paper.yml or spigot.yml and activate timings"); 26 | return true; 27 | } 28 | 29 | sendTimings(sender); 30 | return true; 31 | } 32 | 33 | protected abstract void sendTimings(CommandSender sender); 34 | 35 | protected abstract boolean isTimingsEnabled(); 36 | 37 | protected String highlightPct(float percent, int low, int med, int high) { 38 | ChatColor prefix = ChatColor.GRAY; 39 | if (percent > high) { 40 | prefix = ChatColor.DARK_RED; 41 | } else if (percent > med) { 42 | prefix = ChatColor.GOLD; 43 | } else if (percent > low) { 44 | prefix = ChatColor.YELLOW; 45 | } 46 | 47 | return prefix + String.valueOf(percent) + '%' + ChatColor.GRAY; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/com/github/games647/lagmonitor/graph/ClassesGraph.java: -------------------------------------------------------------------------------- 1 | package com.github.games647.lagmonitor.graph; 2 | 3 | import java.lang.management.ClassLoadingMXBean; 4 | import java.lang.management.ManagementFactory; 5 | 6 | import org.bukkit.map.MapCanvas; 7 | 8 | public class ClassesGraph extends GraphRenderer { 9 | 10 | private final ClassLoadingMXBean classBean = ManagementFactory.getClassLoadingMXBean(); 11 | 12 | public ClassesGraph() { 13 | super("Classes"); 14 | } 15 | 16 | @Override 17 | public int renderGraphTick(MapCanvas canvas, int nextPosX) { 18 | int loadedClasses = classBean.getLoadedClassCount(); 19 | 20 | //round up to the nearest multiple of 5 21 | int roundedMax = (int) (5 * (Math.ceil((float) loadedClasses / 5))); 22 | int loadedHeight = getHeightScaled(roundedMax, loadedClasses); 23 | 24 | fillBar(canvas, nextPosX, MAX_HEIGHT - loadedHeight, MAX_COLOR); 25 | 26 | //these is the max number 27 | return loadedClasses; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/github/games647/lagmonitor/graph/CombinedGraph.java: -------------------------------------------------------------------------------- 1 | package com.github.games647.lagmonitor.graph; 2 | 3 | import org.bukkit.map.MapCanvas; 4 | 5 | public class CombinedGraph extends GraphRenderer { 6 | 7 | private static final int SPACES = 2; 8 | 9 | private final GraphRenderer[] graphRenderers; 10 | 11 | private final int componentWidth; 12 | private final int[] componentLastPos; 13 | 14 | public CombinedGraph(GraphRenderer... renderers) { 15 | super("Combined"); 16 | 17 | this.graphRenderers = renderers; 18 | this.componentLastPos = new int[graphRenderers.length]; 19 | 20 | //MAX width - spaces between (length - 1) the components 21 | componentWidth = MAX_WIDTH - (SPACES * (graphRenderers.length - 1)) / graphRenderers.length; 22 | for (int i = 0; i < componentLastPos.length; i++) { 23 | componentLastPos[i] = i * componentWidth + i * SPACES; 24 | } 25 | } 26 | 27 | @Override 28 | public int renderGraphTick(MapCanvas canvas, int nextPosX) { 29 | for (int i = 0; i < graphRenderers.length; i++) { 30 | GraphRenderer graphRenderer = graphRenderers[i]; 31 | int position = this.componentLastPos[i]; 32 | position++; 33 | 34 | //index starts with 0 so in the end - 1 35 | int maxComponentWidth = (i + 1) * componentWidth + i * SPACES - 1; 36 | if (position > maxComponentWidth) { 37 | //reset it to the start pos 38 | position = i * componentWidth + i * SPACES; 39 | } 40 | 41 | graphRenderer.renderGraphTick(canvas, position); 42 | this.componentLastPos[i] = position; 43 | } 44 | 45 | return 100; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/com/github/games647/lagmonitor/graph/CpuGraph.java: -------------------------------------------------------------------------------- 1 | package com.github.games647.lagmonitor.graph; 2 | 3 | import com.github.games647.lagmonitor.NativeManager; 4 | 5 | import org.bukkit.Bukkit; 6 | import org.bukkit.map.MapCanvas; 7 | import org.bukkit.plugin.Plugin; 8 | 9 | public class CpuGraph extends GraphRenderer { 10 | 11 | private final Plugin plugin; 12 | private final NativeManager nativeData; 13 | 14 | private final Object lock = new Object(); 15 | 16 | private int systemHeight; 17 | private int processHeight; 18 | 19 | public CpuGraph(Plugin plugin, NativeManager nativeData) { 20 | super("CPU Usage"); 21 | 22 | this.plugin = plugin; 23 | this.nativeData = nativeData; 24 | } 25 | 26 | @Override 27 | public int renderGraphTick(MapCanvas canvas, int nextPosX) { 28 | Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { 29 | int systemLoad = (int) (nativeData.getCPULoad() * 100); 30 | int processLOad = (int) (nativeData.getProcessCPULoad() * 100); 31 | 32 | int localSystemHeight = getHeightScaled(100, systemLoad); 33 | int localProcessHeight = getHeightScaled(100, processLOad); 34 | 35 | //flush updates 36 | synchronized (lock) { 37 | this.systemHeight = localSystemHeight; 38 | this.processHeight = localProcessHeight; 39 | } 40 | }); 41 | 42 | //read it only one time 43 | int localSystemHeight; 44 | int localProcessHeight; 45 | synchronized (lock) { 46 | localSystemHeight = this.systemHeight; 47 | localProcessHeight = this.processHeight; 48 | } 49 | 50 | fillBar(canvas, nextPosX, MAX_HEIGHT - localSystemHeight, MAX_COLOR); 51 | fillBar(canvas, nextPosX, MAX_HEIGHT - localProcessHeight, USED_COLOR); 52 | 53 | //set max height as 100% 54 | return 100; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/com/github/games647/lagmonitor/graph/GraphRenderer.java: -------------------------------------------------------------------------------- 1 | package com.github.games647.lagmonitor.graph; 2 | 3 | import org.bukkit.entity.Player; 4 | import org.bukkit.map.MapCanvas; 5 | import org.bukkit.map.MapPalette; 6 | import org.bukkit.map.MapRenderer; 7 | import org.bukkit.map.MapView; 8 | import org.bukkit.map.MinecraftFont; 9 | 10 | public abstract class GraphRenderer extends MapRenderer { 11 | 12 | protected static final int TEXT_HEIGHT = MinecraftFont.Font.getHeight(); 13 | 14 | //max height and width = 128 (index from 0-127) 15 | protected static final int MAX_WIDTH = 128; 16 | protected static final int MAX_HEIGHT = 128; 17 | 18 | //orange 19 | protected static final byte MAX_COLOR = MapPalette.matchColor(235, 171, 96); 20 | 21 | //blue 22 | protected static final byte USED_COLOR = MapPalette.matchColor(105, 182, 212); 23 | 24 | private int nextUpdate; 25 | private int nextPosX; 26 | 27 | private final String title; 28 | 29 | public GraphRenderer(String title) { 30 | this.title = title; 31 | } 32 | 33 | @Override 34 | public void render(MapView map, MapCanvas canvas, Player player) { 35 | if (nextUpdate <= 0) { 36 | //paint only every half seconds (20 Ticks / 2) 37 | nextUpdate = 10; 38 | 39 | if (nextPosX >= MAX_WIDTH) { 40 | //start again from the beginning 41 | nextPosX = 0; 42 | } 43 | 44 | clearBar(canvas, nextPosX); 45 | //make it more visual where the renderer is at the moment 46 | clearBar(canvas, nextPosX + 1); 47 | int maxValue = renderGraphTick(canvas, nextPosX); 48 | 49 | //override the color 50 | drawText(canvas, MAX_WIDTH / 2, MAX_HEIGHT / 2, title); 51 | 52 | //count indicators 53 | String maxText = Integer.toString(maxValue); 54 | drawText(canvas, MAX_WIDTH - Math.floorDiv(getTextWidth(maxText), 2), TEXT_HEIGHT, maxText); 55 | 56 | String midText = Integer.toString(maxValue / 2); 57 | drawText(canvas, MAX_WIDTH - Math.floorDiv(getTextWidth(midText), 2), MAX_HEIGHT / 2, midText); 58 | 59 | String zeroText = Integer.toString(0); 60 | drawText(canvas, MAX_WIDTH - Math.floorDiv(getTextWidth(zeroText), 2), MAX_HEIGHT, zeroText); 61 | 62 | nextPosX++; 63 | } 64 | 65 | nextUpdate--; 66 | } 67 | 68 | public abstract int renderGraphTick(MapCanvas canvas, int nextPosX); 69 | 70 | protected int getHeightScaled(int maxValue, int value) { 71 | return MAX_HEIGHT * value / maxValue; 72 | } 73 | 74 | protected void clearBar(MapCanvas canvas, int posX) { 75 | //resets the complete y coordinates on this x in order to free unused 76 | for (int yPos = 0; yPos < MAX_HEIGHT; yPos++) { 77 | canvas.setPixel(posX, yPos, (byte) 0); 78 | } 79 | } 80 | 81 | protected void clearMap(MapCanvas canvas) { 82 | for (int xPos = 0; xPos < MAX_WIDTH; xPos++) { 83 | fillBar(canvas, xPos, 0, (byte) 0); 84 | } 85 | } 86 | 87 | protected void fillBar(MapCanvas canvas, int xPos, int yStart, byte color) { 88 | for (int yPos = yStart; yPos < MAX_HEIGHT; yPos++) { 89 | canvas.setPixel(xPos, yPos, color); 90 | } 91 | } 92 | 93 | protected void drawText(MapCanvas canvas, int midX, int midY, String text) { 94 | int textWidth = getTextWidth(text); 95 | canvas.drawText(midX - (textWidth / 2), midY - (TEXT_HEIGHT / 2), MinecraftFont.Font, text); 96 | } 97 | 98 | private int getTextWidth(String text) { 99 | return MinecraftFont.Font.getWidth(text); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/main/java/com/github/games647/lagmonitor/graph/HeapGraph.java: -------------------------------------------------------------------------------- 1 | package com.github.games647.lagmonitor.graph; 2 | 3 | import java.lang.management.ManagementFactory; 4 | import java.lang.management.MemoryMXBean; 5 | 6 | import org.bukkit.map.MapCanvas; 7 | 8 | public class HeapGraph extends GraphRenderer { 9 | 10 | private final MemoryMXBean heapUsage = ManagementFactory.getMemoryMXBean(); 11 | 12 | public HeapGraph() { 13 | super("HeapUsage (MB)"); 14 | } 15 | 16 | @Override 17 | public int renderGraphTick(MapCanvas canvas, int nextPosX) { 18 | //byte -> mega byte 19 | int max = (int) (heapUsage.getHeapMemoryUsage().getCommitted() / 1024 / 1024); 20 | int used = (int) (heapUsage.getHeapMemoryUsage().getUsed() / 1024 / 1024); 21 | 22 | //round to the next 100 e.g. 801 -> 900 23 | int roundedMax = ((max + 99) / 100) * 100; 24 | 25 | int maxHeight = getHeightScaled(roundedMax, max); 26 | int usedHeight = getHeightScaled(roundedMax, used); 27 | 28 | //x=0 y=0 is the left top point so convert it 29 | int convertedMaxHeight = MAX_HEIGHT - maxHeight; 30 | int convertedUsedHeight = MAX_HEIGHT - usedHeight; 31 | 32 | fillBar(canvas, nextPosX, convertedMaxHeight, MAX_COLOR); 33 | fillBar(canvas, nextPosX, convertedUsedHeight, USED_COLOR); 34 | 35 | return maxHeight; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/github/games647/lagmonitor/graph/ThreadsGraph.java: -------------------------------------------------------------------------------- 1 | package com.github.games647.lagmonitor.graph; 2 | 3 | import java.lang.management.ManagementFactory; 4 | import java.lang.management.ThreadMXBean; 5 | 6 | import org.bukkit.map.MapCanvas; 7 | 8 | public class ThreadsGraph extends GraphRenderer { 9 | 10 | private final ThreadMXBean threadBean = ManagementFactory.getThreadMXBean(); 11 | 12 | public ThreadsGraph() { 13 | super("Thread activity"); 14 | } 15 | 16 | @Override 17 | public int renderGraphTick(MapCanvas canvas, int nextPosX) { 18 | int threadCount = threadBean.getThreadCount(); 19 | int daemonCount = threadBean.getDaemonThreadCount(); 20 | 21 | //round up to the nearest multiple of 5 22 | int roundedMax = (int) (5 * (Math.ceil((float) threadCount / 5))); 23 | int threadHeight = getHeightScaled(roundedMax, threadCount); 24 | int daemonHeight = getHeightScaled(roundedMax, daemonCount); 25 | 26 | fillBar(canvas, nextPosX, MAX_HEIGHT - threadHeight, MAX_COLOR); 27 | fillBar(canvas, nextPosX, MAX_HEIGHT - daemonHeight, USED_COLOR); 28 | 29 | //these is the max number of all threads 30 | return threadCount; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/github/games647/lagmonitor/listener/BlockingConnectionSelector.java: -------------------------------------------------------------------------------- 1 | package com.github.games647.lagmonitor.listener; 2 | 3 | import com.github.games647.lagmonitor.threading.BlockingActionManager; 4 | import com.github.games647.lagmonitor.threading.Injectable; 5 | 6 | import java.io.IOException; 7 | import java.net.Proxy; 8 | import java.net.ProxySelector; 9 | import java.net.SocketAddress; 10 | import java.net.URI; 11 | import java.util.Collections; 12 | import java.util.List; 13 | import java.util.regex.Pattern; 14 | 15 | public class BlockingConnectionSelector extends ProxySelector implements Injectable { 16 | 17 | private static final Pattern WWW_PATERN = Pattern.compile("www", Pattern.LITERAL); 18 | 19 | private final BlockingActionManager actionManager; 20 | private ProxySelector oldProxySelector; 21 | 22 | public BlockingConnectionSelector(BlockingActionManager actionManager) { 23 | this.actionManager = actionManager; 24 | } 25 | 26 | @Override 27 | public List select(URI uri) { 28 | String url = WWW_PATERN.matcher(uri.toString()).replaceAll(""); 29 | if (uri.getScheme().startsWith("http") || (uri.getPort() != 80 && uri.getPort() != 443)) { 30 | actionManager.checkBlockingAction("Socket: " + url); 31 | } 32 | 33 | return oldProxySelector == null ? Collections.singletonList(Proxy.NO_PROXY) : oldProxySelector.select(uri); 34 | } 35 | 36 | @Override 37 | public void connectFailed(URI uri, SocketAddress sa, IOException ioe) { 38 | if (oldProxySelector != null) { 39 | oldProxySelector.connectFailed(uri, sa, ioe); 40 | } 41 | } 42 | 43 | @Override 44 | public void inject() { 45 | ProxySelector proxySelector = ProxySelector.getDefault(); 46 | if (proxySelector != this) { 47 | oldProxySelector = proxySelector; 48 | ProxySelector.setDefault(this); 49 | } 50 | } 51 | 52 | @Override 53 | public void restore() { 54 | if (ProxySelector.getDefault() == this) { 55 | ProxySelector.setDefault(oldProxySelector); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/com/github/games647/lagmonitor/listener/GraphListener.java: -------------------------------------------------------------------------------- 1 | package com.github.games647.lagmonitor.listener; 2 | 3 | import com.github.games647.lagmonitor.graph.GraphRenderer; 4 | import com.github.games647.lagmonitor.util.LagUtils; 5 | 6 | import java.lang.invoke.MethodHandles; 7 | import java.lang.invoke.MethodType; 8 | 9 | import org.bukkit.Bukkit; 10 | import org.bukkit.Material; 11 | import org.bukkit.entity.Item; 12 | import org.bukkit.entity.Player; 13 | import org.bukkit.event.EventHandler; 14 | import org.bukkit.event.EventPriority; 15 | import org.bukkit.event.Listener; 16 | import org.bukkit.event.player.PlayerDropItemEvent; 17 | import org.bukkit.event.player.PlayerInteractEvent; 18 | import org.bukkit.inventory.ItemStack; 19 | import org.bukkit.inventory.PlayerInventory; 20 | import org.bukkit.inventory.meta.ItemMeta; 21 | import org.bukkit.inventory.meta.MapMeta; 22 | import org.bukkit.map.MapView; 23 | 24 | public class GraphListener implements Listener { 25 | 26 | private final boolean mainHandSupported; 27 | 28 | public GraphListener() { 29 | boolean mainHandMethodEx = false; 30 | try { 31 | MethodType type = MethodType.methodType(ItemStack.class); 32 | MethodHandles.publicLookup().findVirtual(PlayerInventory.class, "getItemInMainHand", type); 33 | mainHandMethodEx = true; 34 | } catch (ReflectiveOperationException notFoundEx) { 35 | //default to false 36 | } 37 | 38 | this.mainHandSupported = mainHandMethodEx; 39 | } 40 | 41 | @EventHandler(ignoreCancelled = true, priority = EventPriority.HIGH) 42 | public void onInteract(PlayerInteractEvent clickEvent) { 43 | Player player = clickEvent.getPlayer(); 44 | PlayerInventory inventory = player.getInventory(); 45 | 46 | ItemStack mainHandItem; 47 | if (mainHandSupported) { 48 | mainHandItem = inventory.getItemInMainHand(); 49 | } else { 50 | mainHandItem = inventory.getItemInHand(); 51 | } 52 | 53 | if (isOurGraph(mainHandItem)) { 54 | inventory.setItemInMainHand(new ItemStack(Material.AIR)); 55 | } 56 | } 57 | 58 | @EventHandler(ignoreCancelled = true, priority = EventPriority.HIGH) 59 | public void onDrop(PlayerDropItemEvent dropItemEvent) { 60 | Item itemDrop = dropItemEvent.getItemDrop(); 61 | ItemStack mapItem = itemDrop.getItemStack(); 62 | if (isOurGraph(mapItem)) { 63 | mapItem.setAmount(0); 64 | } 65 | } 66 | 67 | private boolean isOurGraph(ItemStack item) { 68 | if (!LagUtils.isFilledMapSupported()) { 69 | return isOurGraphLegacy(item); 70 | } 71 | 72 | if (item.getType() != Material.FILLED_MAP) { 73 | return false; 74 | } 75 | 76 | ItemMeta meta = item.getItemMeta(); 77 | if (!(meta instanceof MapMeta)) { 78 | return false; 79 | } 80 | 81 | MapMeta mapMeta = (MapMeta) meta; 82 | MapView mapView = mapMeta.getMapView(); 83 | return mapView != null && isOurRenderer(mapView); 84 | } 85 | 86 | private boolean isOurGraphLegacy(ItemStack mapItem) { 87 | if (mapItem.getType() != Material.MAP) 88 | return false; 89 | 90 | short mapId = mapItem.getDurability(); 91 | MapView mapView = Bukkit.getMap(mapId); 92 | return mapView != null && isOurRenderer(mapView); 93 | } 94 | 95 | private boolean isOurRenderer(MapView mapView) { 96 | return mapView.getRenderers().stream() 97 | .anyMatch(GraphRenderer.class::isInstance); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/main/java/com/github/games647/lagmonitor/listener/PageManager.java: -------------------------------------------------------------------------------- 1 | package com.github.games647.lagmonitor.listener; 2 | 3 | import com.github.games647.lagmonitor.Pages; 4 | 5 | import java.util.HashMap; 6 | import java.util.Map; 7 | 8 | import org.bukkit.event.EventHandler; 9 | import org.bukkit.event.Listener; 10 | import org.bukkit.event.player.PlayerQuitEvent; 11 | 12 | public class PageManager implements Listener { 13 | 14 | private final Map pages = new HashMap<>(); 15 | 16 | @EventHandler 17 | public void onPlayerQuit(PlayerQuitEvent quitEvent) { 18 | pages.remove(quitEvent.getPlayer().getName()); 19 | } 20 | 21 | public Pages getPagination(String username) { 22 | return pages.get(username); 23 | } 24 | 25 | public void setPagination(String username, Pages pagination) { 26 | pages.put(username, pagination); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/github/games647/lagmonitor/listener/ThreadSafetyListener.java: -------------------------------------------------------------------------------- 1 | package com.github.games647.lagmonitor.listener; 2 | 3 | import com.github.games647.lagmonitor.threading.BlockingActionManager; 4 | 5 | import org.bukkit.event.Event; 6 | import org.bukkit.event.EventHandler; 7 | import org.bukkit.event.Listener; 8 | import org.bukkit.event.block.BlockFromToEvent; 9 | import org.bukkit.event.block.BlockPhysicsEvent; 10 | import org.bukkit.event.entity.CreatureSpawnEvent; 11 | import org.bukkit.event.entity.EntitySpawnEvent; 12 | import org.bukkit.event.entity.ItemSpawnEvent; 13 | import org.bukkit.event.inventory.InventoryOpenEvent; 14 | import org.bukkit.event.player.PlayerCommandPreprocessEvent; 15 | import org.bukkit.event.player.PlayerItemHeldEvent; 16 | import org.bukkit.event.player.PlayerJoinEvent; 17 | import org.bukkit.event.player.PlayerMoveEvent; 18 | import org.bukkit.event.player.PlayerQuitEvent; 19 | import org.bukkit.event.player.PlayerTeleportEvent; 20 | import org.bukkit.event.server.PluginDisableEvent; 21 | import org.bukkit.event.server.PluginEnableEvent; 22 | import org.bukkit.event.world.ChunkLoadEvent; 23 | import org.bukkit.event.world.ChunkUnloadEvent; 24 | import org.bukkit.event.world.SpawnChangeEvent; 25 | import org.bukkit.event.world.WorldLoadEvent; 26 | import org.bukkit.event.world.WorldSaveEvent; 27 | import org.bukkit.event.world.WorldUnloadEvent; 28 | 29 | /** 30 | * We can listen to events which are intended to run sync to the main thread. 31 | * If those events are fired on a async task the operation was likely not thread-safe. 32 | */ 33 | public class ThreadSafetyListener implements Listener { 34 | 35 | private final BlockingActionManager actionManager; 36 | 37 | public ThreadSafetyListener(BlockingActionManager actionManager) { 38 | this.actionManager = actionManager; 39 | } 40 | 41 | @EventHandler 42 | public void onCommand(PlayerCommandPreprocessEvent commandEvent) { 43 | checkSafety(commandEvent); 44 | } 45 | 46 | @EventHandler 47 | public void onInventoryOpen(InventoryOpenEvent inventoryOpenEvent) { 48 | checkSafety(inventoryOpenEvent); 49 | } 50 | 51 | @EventHandler 52 | public void onPlayerMove(PlayerMoveEvent moveEvent) { 53 | checkSafety(moveEvent); 54 | } 55 | 56 | @EventHandler 57 | public void onPlayerTeleport(PlayerTeleportEvent teleportEvent) { 58 | checkSafety(teleportEvent); 59 | } 60 | 61 | @EventHandler 62 | public void onPlayerJoin(PlayerJoinEvent joinEvent) { 63 | checkSafety(joinEvent); 64 | } 65 | 66 | @EventHandler 67 | public void onPlayerQuit(PlayerQuitEvent quitEvent) { 68 | checkSafety(quitEvent); 69 | } 70 | 71 | @EventHandler 72 | public void onItemHeldChange(PlayerItemHeldEvent itemHeldEvent) { 73 | checkSafety(itemHeldEvent); 74 | } 75 | 76 | @EventHandler 77 | public void onBlockPhysics(BlockPhysicsEvent blockPhysicsEvent) { 78 | checkSafety(blockPhysicsEvent); 79 | } 80 | 81 | @EventHandler 82 | public void onBlockFromTo(BlockFromToEvent blockFromToEvent) { 83 | checkSafety(blockFromToEvent); 84 | } 85 | 86 | @EventHandler 87 | public void onCreatureSpawn(CreatureSpawnEvent creatureSpawnEvent) { 88 | checkSafety(creatureSpawnEvent); 89 | } 90 | 91 | @EventHandler 92 | public void onItemSpawn(ItemSpawnEvent itemSpawnEvent) { 93 | checkSafety(itemSpawnEvent); 94 | } 95 | 96 | @EventHandler 97 | public void onChunkLoad(ChunkLoadEvent chunkLoadEvent) { 98 | checkSafety(chunkLoadEvent); 99 | } 100 | 101 | @EventHandler 102 | public void onChunkUnload(ChunkUnloadEvent chunkUnloadEvent) { 103 | checkSafety(chunkUnloadEvent); 104 | } 105 | 106 | @EventHandler 107 | public void onWorldLoad(WorldLoadEvent worldLoadEvent) { 108 | checkSafety(worldLoadEvent); 109 | } 110 | 111 | @EventHandler 112 | public void onWorldSave(WorldSaveEvent worldSaveEvent) { 113 | checkSafety(worldSaveEvent); 114 | } 115 | 116 | @EventHandler 117 | public void onWorldUnload(WorldUnloadEvent worldUnloadEvent) { 118 | checkSafety(worldUnloadEvent); 119 | } 120 | 121 | @EventHandler 122 | public void onPluginEnable(PluginEnableEvent pluginEnableEvent) { 123 | checkSafety(pluginEnableEvent); 124 | } 125 | 126 | @EventHandler 127 | public void onPluginDisable(PluginDisableEvent pluginDisableEvent) { 128 | checkSafety(pluginDisableEvent); 129 | } 130 | 131 | @EventHandler 132 | public void onSpawnChange(SpawnChangeEvent spawnChangeEvent) { 133 | checkSafety(spawnChangeEvent); 134 | } 135 | 136 | @EventHandler 137 | public void onSpawnChange(EntitySpawnEvent spawnEvent) { 138 | checkSafety(spawnEvent); 139 | } 140 | 141 | private void checkSafety(Event eventType) { 142 | //async executing of sync event 143 | String eventName = eventType.getEventName(); 144 | if (!eventType.isAsynchronous()) { 145 | actionManager.checkThreadSafety(eventName); 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/main/java/com/github/games647/lagmonitor/logging/ForwardLogService.java: -------------------------------------------------------------------------------- 1 | package com.github.games647.lagmonitor.logging; 2 | 3 | import org.slf4j.ILoggerFactory; 4 | import org.slf4j.IMarkerFactory; 5 | import org.slf4j.jul.JULServiceProvider; 6 | import org.slf4j.spi.MDCAdapter; 7 | import org.slf4j.spi.SLF4JServiceProvider; 8 | 9 | public class ForwardLogService implements SLF4JServiceProvider { 10 | 11 | private ILoggerFactory loggerFactory; 12 | private JULServiceProvider delegate; 13 | 14 | public ILoggerFactory getLoggerFactory() { 15 | return this.loggerFactory; 16 | } 17 | 18 | public IMarkerFactory getMarkerFactory() { 19 | return delegate.getMarkerFactory(); 20 | } 21 | 22 | public MDCAdapter getMDCAdapter() { 23 | return delegate.getMDCAdapter(); 24 | } 25 | 26 | @Override 27 | public String getRequesteApiVersion() { 28 | return delegate.getRequestedApiVersion(); 29 | } 30 | 31 | public String getRequestedApiVersion() { 32 | return delegate.getRequestedApiVersion(); 33 | } 34 | 35 | public void initialize() { 36 | this.delegate = new JULServiceProvider(); 37 | this.loggerFactory = new ForwardingLoggerFactory(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/github/games647/lagmonitor/logging/ForwardingLoggerFactory.java: -------------------------------------------------------------------------------- 1 | package com.github.games647.lagmonitor.logging; 2 | 3 | import java.lang.reflect.Constructor; 4 | import java.lang.reflect.InvocationTargetException; 5 | import java.util.concurrent.ConcurrentHashMap; 6 | import java.util.concurrent.ConcurrentMap; 7 | 8 | import org.slf4j.ILoggerFactory; 9 | import org.slf4j.Logger; 10 | import org.slf4j.jul.JDK14LoggerAdapter; 11 | 12 | public class ForwardingLoggerFactory implements ILoggerFactory { 13 | 14 | private final ConcurrentMap loggerMap = new ConcurrentHashMap<>(); 15 | 16 | public static java.util.logging.Logger PARENT_LOGGER; 17 | 18 | @Override 19 | public Logger getLogger(String name) { 20 | return loggerMap.computeIfAbsent(name, key -> { 21 | java.util.logging.Logger julLogger; 22 | if (PARENT_LOGGER == null) { 23 | julLogger = java.util.logging.Logger.getLogger(name); 24 | } else { 25 | julLogger = PARENT_LOGGER; 26 | } 27 | 28 | Logger newInstance = null; 29 | try { 30 | newInstance = createJDKLogger(julLogger); 31 | } catch (NoSuchMethodException | InvocationTargetException | 32 | InstantiationException | IllegalAccessException e) { 33 | e.printStackTrace(); 34 | System.out.println("Failed to created logging instance"); 35 | } 36 | 37 | return newInstance; 38 | }); 39 | } 40 | 41 | protected static Logger createJDKLogger(java.util.logging.Logger parent) 42 | throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException { 43 | Class adapterClass = JDK14LoggerAdapter.class; 44 | Constructor cons = adapterClass.getDeclaredConstructor(java.util.logging.Logger.class); 45 | cons.setAccessible(true); 46 | return (Logger) cons.newInstance(parent); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/com/github/games647/lagmonitor/ping/PaperPing.java: -------------------------------------------------------------------------------- 1 | package com.github.games647.lagmonitor.ping; 2 | 3 | import org.bukkit.entity.Player; 4 | 5 | public class PaperPing implements PingFetcher { 6 | 7 | @Override 8 | public boolean isAvailable() { 9 | try { 10 | //Only available in Paper 11 | Player.Spigot.class.getDeclaredMethod("getPing"); 12 | return true; 13 | } catch (NoSuchMethodException noSuchMethodEx) { 14 | return false; 15 | } 16 | } 17 | 18 | @Override 19 | public int getPing(Player player) { 20 | return player.spigot().getPing(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/github/games647/lagmonitor/ping/PingFetcher.java: -------------------------------------------------------------------------------- 1 | package com.github.games647.lagmonitor.ping; 2 | 3 | import org.bukkit.entity.Player; 4 | 5 | public interface PingFetcher { 6 | 7 | boolean isAvailable(); 8 | 9 | int getPing(Player player); 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/github/games647/lagmonitor/ping/ReflectionPing.java: -------------------------------------------------------------------------------- 1 | package com.github.games647.lagmonitor.ping; 2 | 3 | import com.github.games647.lagmonitor.LagMonitor; 4 | import com.github.games647.lagmonitor.traffic.Reflection; 5 | 6 | import java.lang.invoke.MethodHandle; 7 | import java.lang.invoke.MethodHandles; 8 | import java.lang.invoke.MethodHandles.Lookup; 9 | import java.lang.invoke.MethodType; 10 | import java.util.logging.Level; 11 | import java.util.logging.Logger; 12 | 13 | import org.bukkit.entity.Player; 14 | import org.bukkit.plugin.java.JavaPlugin; 15 | 16 | public class ReflectionPing implements PingFetcher { 17 | 18 | private static final MethodHandle pingFromPlayerHandle; 19 | 20 | static { 21 | MethodHandle localPing = null; 22 | Class craftPlayerClass = Reflection.getCraftBukkitClass("entity.CraftPlayer"); 23 | String playerClazz = "EntityPlayer"; 24 | Class entityPlayer = Reflection.getMinecraftClass(playerClazz, "level." + playerClazz); 25 | 26 | Lookup lookup = MethodHandles.publicLookup(); 27 | try { 28 | MethodType type = MethodType.methodType(entityPlayer); 29 | MethodHandle getHandle = lookup.findVirtual(craftPlayerClass, "getHandle", type); 30 | MethodHandle pingField = lookup.findGetter(entityPlayer, "ping", Integer.TYPE); 31 | 32 | // combine the handles to invoke it only once 33 | // *getPing(getHandle*) -> add the result of getHandle to the next getPing call 34 | // a call to this handle will get the ping from a player instance 35 | localPing = MethodHandles.collectArguments(pingField, 0, getHandle) 36 | // allow interface with invokeExact 37 | .asType(MethodType.methodType(int.class, Player.class)); 38 | } catch (NoSuchMethodException | IllegalAccessException | NoSuchFieldException reflectiveEx) { 39 | Logger logger = JavaPlugin.getPlugin(LagMonitor.class).getLogger(); 40 | logger.log(Level.WARNING, "Cannot find ping field/method", reflectiveEx); 41 | } 42 | 43 | pingFromPlayerHandle = localPing; 44 | } 45 | 46 | @Override 47 | public boolean isAvailable() { 48 | return pingFromPlayerHandle != null; 49 | } 50 | 51 | @Override 52 | public int getPing(Player player) { 53 | try { 54 | return (int) pingFromPlayerHandle.invokeExact(player); 55 | } catch (Exception ex) { 56 | return -1; 57 | } catch (Throwable throwable) { 58 | throw (Error) throwable; 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/com/github/games647/lagmonitor/ping/SpigotPing.java: -------------------------------------------------------------------------------- 1 | package com.github.games647.lagmonitor.ping; 2 | 3 | import org.bukkit.entity.Player; 4 | 5 | public class SpigotPing implements PingFetcher { 6 | 7 | @Override 8 | public boolean isAvailable() { 9 | try { 10 | //Only available in Paper 11 | Player.class.getDeclaredMethod("getPing"); 12 | return true; 13 | } catch (NoSuchMethodException noSuchMethodEx) { 14 | return false; 15 | } 16 | } 17 | 18 | @Override 19 | public int getPing(Player player) { 20 | return player.getPing(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/github/games647/lagmonitor/storage/MonitorSaveTask.java: -------------------------------------------------------------------------------- 1 | package com.github.games647.lagmonitor.storage; 2 | 3 | import com.github.games647.lagmonitor.LagMonitor; 4 | import com.github.games647.lagmonitor.NativeManager; 5 | import com.github.games647.lagmonitor.util.LagUtils; 6 | import com.google.common.collect.Lists; 7 | import com.google.common.collect.Maps; 8 | 9 | import java.lang.management.ManagementFactory; 10 | import java.lang.management.OperatingSystemMXBean; 11 | import java.nio.file.Path; 12 | import java.util.Collection; 13 | import java.util.List; 14 | import java.util.Map; 15 | import java.util.UUID; 16 | import java.util.concurrent.ExecutionException; 17 | import java.util.concurrent.Future; 18 | import java.util.logging.Level; 19 | 20 | import org.bukkit.Bukkit; 21 | import org.bukkit.World; 22 | import org.bukkit.entity.Player; 23 | 24 | import static com.github.games647.lagmonitor.util.LagUtils.round; 25 | 26 | public class MonitorSaveTask implements Runnable { 27 | 28 | protected final LagMonitor plugin; 29 | protected final Storage storage; 30 | 31 | public MonitorSaveTask(LagMonitor plugin, Storage storage) { 32 | this.plugin = plugin; 33 | this.storage = storage; 34 | } 35 | 36 | @Override 37 | public void run() { 38 | try { 39 | int monitorId = save(); 40 | if (monitorId == -1) { 41 | //error occurred 42 | return; 43 | } 44 | 45 | Map worldsData = getWorldData(); 46 | if (!storage.saveWorlds(monitorId, worldsData.values())) { 47 | //error occurred 48 | return; 49 | } 50 | 51 | List playerData = getPlayerData(worldsData); 52 | storage.savePlayers(playerData); 53 | } catch (ExecutionException | InterruptedException ex) { 54 | plugin.getLogger().log(Level.SEVERE, "Error saving monitoring data", ex); 55 | } 56 | } 57 | 58 | private List getPlayerData(final Map worldsData) 59 | throws InterruptedException, ExecutionException { 60 | Future> playerFuture = Bukkit.getScheduler() 61 | .callSyncMethod(plugin, () -> { 62 | Collection onlinePlayers = Bukkit.getOnlinePlayers(); 63 | List playerData = Lists.newArrayListWithCapacity(onlinePlayers.size()); 64 | for (Player player : onlinePlayers) { 65 | UUID worldId = player.getWorld().getUID(); 66 | 67 | int worldRowId = 0; 68 | WorldData worldData = worldsData.get(worldId); 69 | if (worldData != null) { 70 | worldRowId = worldData.getRowId(); 71 | } 72 | 73 | String name = player.getName(); 74 | int lastPing = plugin.getPingManager().map(m -> ((int) m.getHistory(name).getLastSample())) 75 | .orElse(-1); 76 | 77 | UUID playerId = player.getUniqueId(); 78 | playerData.add(new PlayerData(worldRowId, playerId, name, lastPing)); 79 | } 80 | 81 | return playerData; 82 | }); 83 | 84 | return playerFuture.get(); 85 | } 86 | 87 | private Map getWorldData() 88 | throws ExecutionException, InterruptedException { 89 | //this is not thread-safe and have to run sync 90 | 91 | Future> worldFuture = Bukkit.getScheduler() 92 | .callSyncMethod(plugin, () -> { 93 | List worlds = Bukkit.getWorlds(); 94 | Map worldsData = Maps.newHashMapWithExpectedSize(worlds.size()); 95 | for (World world : worlds) { 96 | worldsData.put(world.getUID(), WorldData.fromWorld(world)); 97 | } 98 | 99 | return worldsData; 100 | }); 101 | 102 | Map worldsData = worldFuture.get(); 103 | 104 | //this can run async because it's thread-safe 105 | worldsData.values().parallelStream() 106 | .forEach(data -> { 107 | Path worldFolder = Bukkit.getWorld(data.getWorldName()).getWorldFolder().toPath(); 108 | 109 | int worldSize = LagUtils.byteToMega(LagUtils.getFolderSize(plugin.getLogger(), worldFolder)); 110 | data.setWorldSize(worldSize); 111 | }); 112 | 113 | return worldsData; 114 | } 115 | 116 | private int save() { 117 | Runtime runtime = Runtime.getRuntime(); 118 | int maxMemory = LagUtils.byteToMega(runtime.maxMemory()); 119 | //we need the free ram not the free heap 120 | int usedRam = LagUtils.byteToMega(runtime.totalMemory() - runtime.freeMemory()); 121 | int freeRam = maxMemory - usedRam; 122 | 123 | float freeRamPct = round((freeRam * 100) / maxMemory, 4); 124 | 125 | OperatingSystemMXBean osBean = ManagementFactory.getOperatingSystemMXBean(); 126 | float loadAvg = round(osBean.getSystemLoadAverage(), 4); 127 | if (loadAvg < 0) { 128 | //windows doesn't support this 129 | loadAvg = 0; 130 | } 131 | 132 | NativeManager nativeData = plugin.getNativeData(); 133 | float systemUsage = round(nativeData.getCPULoad() * 100, 4); 134 | float processUsage = round(nativeData.getProcessCPULoad() * 100, 4); 135 | 136 | int totalOsMemory = LagUtils.byteToMega(nativeData.getTotalMemory()); 137 | int freeOsRam = LagUtils.byteToMega(nativeData.getFreeMemory()); 138 | 139 | float freeOsRamPct = round((freeOsRam * 100) / totalOsMemory, 4); 140 | return storage.saveMonitor(processUsage, systemUsage, freeRam, freeRamPct, freeOsRam, freeOsRamPct, loadAvg); 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/main/java/com/github/games647/lagmonitor/storage/NativeSaveTask.java: -------------------------------------------------------------------------------- 1 | package com.github.games647.lagmonitor.storage; 2 | 3 | import com.github.games647.lagmonitor.LagMonitor; 4 | import com.github.games647.lagmonitor.traffic.TrafficReader; 5 | import com.github.games647.lagmonitor.util.LagUtils; 6 | 7 | import java.nio.file.Path; 8 | import java.nio.file.Paths; 9 | import java.time.Duration; 10 | import java.time.Instant; 11 | import java.util.List; 12 | import java.util.Optional; 13 | 14 | import oshi.SystemInfo; 15 | import oshi.hardware.NetworkIF; 16 | import oshi.software.os.OSProcess; 17 | 18 | import static com.github.games647.lagmonitor.util.LagUtils.round; 19 | 20 | public class NativeSaveTask implements Runnable { 21 | 22 | private final LagMonitor plugin; 23 | private final Storage storage; 24 | 25 | private Instant lastCheck = Instant.now(); 26 | 27 | private int lastMcRead; 28 | private int lastMcWrite; 29 | private int lastDiskRead; 30 | private int lastDiskWrite; 31 | private int lastNetRead; 32 | private int lastNetWrite; 33 | 34 | public NativeSaveTask(LagMonitor plugin, Storage storage) { 35 | this.plugin = plugin; 36 | this.storage = storage; 37 | } 38 | 39 | @Override 40 | public void run() { 41 | Instant currentTime = Instant.now(); 42 | int timeDiff = (int) Duration.between(lastCheck, currentTime).getSeconds(); 43 | 44 | int mcReadDiff = 0; 45 | int mcWriteDiff = 0; 46 | 47 | TrafficReader trafficReader = plugin.getTrafficReader(); 48 | if (trafficReader != null) { 49 | int mcRead = LagUtils.byteToMega(trafficReader.getIncomingBytes().longValue()); 50 | mcReadDiff = getDifference(mcRead, lastMcRead, timeDiff); 51 | lastMcRead = mcRead; 52 | 53 | int mcWrite = LagUtils.byteToMega(trafficReader.getOutgoingBytes().longValue()); 54 | mcWriteDiff = getDifference(mcWrite, lastMcWrite, timeDiff); 55 | lastMcWrite = mcWrite; 56 | } 57 | 58 | int totalSpace = LagUtils.byteToMega(plugin.getNativeData().getTotalSpace()); 59 | int freeSpace = LagUtils.byteToMega(plugin.getNativeData().getFreeSpace()); 60 | 61 | //4 decimal places -> Example: 0.2456 62 | float freeSpacePct = round((freeSpace * 100 / (float) totalSpace), 4); 63 | 64 | int diskReadDiff = 0; 65 | int diskWriteDiff = 0; 66 | int netReadDiff = 0; 67 | int netWriteDiff = 0; 68 | 69 | Optional systemInfo = plugin.getNativeData().getSystemInfo(); 70 | if (systemInfo.isPresent()) { 71 | List networkIfs = systemInfo.get().getHardware().getNetworkIFs(); 72 | if (!networkIfs.isEmpty()) { 73 | NetworkIF networkInterface = networkIfs.get(0); 74 | 75 | int netRead = LagUtils.byteToMega(networkInterface.getBytesRecv()); 76 | netReadDiff = getDifference(netRead, lastNetRead, timeDiff); 77 | lastNetRead = netRead; 78 | 79 | int netWrite = LagUtils.byteToMega(networkInterface.getBytesSent()); 80 | netWriteDiff = getDifference(netWrite, lastNetWrite, timeDiff); 81 | lastNetWrite = netWrite; 82 | } 83 | 84 | Path root = Paths.get(".").getRoot(); 85 | Optional optProcess = plugin.getNativeData().getProcess(); 86 | if (root != null && optProcess.isPresent()) { 87 | OSProcess process = optProcess.get(); 88 | String rootFileSystem = root.toAbsolutePath().toString(); 89 | 90 | int diskRead = LagUtils.byteToMega(process.getBytesRead()); 91 | diskReadDiff = getDifference(diskRead, lastDiskRead, timeDiff); 92 | lastDiskRead = diskRead; 93 | 94 | int diskWrite = LagUtils.byteToMega(process.getBytesWritten()); 95 | diskWriteDiff = getDifference(diskWrite, lastDiskWrite, timeDiff); 96 | lastDiskWrite = diskWrite; 97 | } 98 | } 99 | 100 | lastCheck = currentTime; 101 | storage.saveNative(mcReadDiff, mcWriteDiff, freeSpace, freeSpacePct, diskReadDiff, diskWriteDiff 102 | , netReadDiff, netWriteDiff); 103 | } 104 | 105 | private int getDifference(long newVal, long oldVal, long timeDiff) { 106 | return (int) ((newVal - oldVal) / timeDiff); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/main/java/com/github/games647/lagmonitor/storage/PlayerData.java: -------------------------------------------------------------------------------- 1 | package com.github.games647.lagmonitor.storage; 2 | 3 | import java.util.UUID; 4 | 5 | public class PlayerData { 6 | 7 | private final int worldId; 8 | private final UUID uuid; 9 | private final String playerName; 10 | private final int ping; 11 | 12 | public PlayerData(int worldId, UUID uuid, String playerName, int ping) { 13 | this.worldId = worldId; 14 | this.uuid = uuid; 15 | this.playerName = playerName; 16 | 17 | if (ping < 0) { 18 | this.ping = Integer.MAX_VALUE; 19 | } else { 20 | this.ping = ping; 21 | } 22 | } 23 | 24 | public int getWorldId() { 25 | return worldId; 26 | } 27 | 28 | public UUID getUuid() { 29 | return uuid; 30 | } 31 | 32 | public String getPlayerName() { 33 | return playerName; 34 | } 35 | 36 | public int getPing() { 37 | return ping; 38 | } 39 | 40 | @Override 41 | public String toString() { 42 | return this.getClass().getSimpleName() + '{' + 43 | "worldId=" + worldId 44 | + ", uuid=" + uuid 45 | + ", playerName=" + playerName 46 | + ", ping=" + ping 47 | + '}'; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/com/github/games647/lagmonitor/storage/TPSSaveTask.java: -------------------------------------------------------------------------------- 1 | package com.github.games647.lagmonitor.storage; 2 | 3 | import com.github.games647.lagmonitor.task.TPSHistoryTask; 4 | 5 | public class TPSSaveTask implements Runnable { 6 | 7 | private final TPSHistoryTask tpsHistoryTask; 8 | private final Storage storage; 9 | 10 | public TPSSaveTask(TPSHistoryTask tpsHistoryTask, Storage storage) { 11 | this.tpsHistoryTask = tpsHistoryTask; 12 | this.storage = storage; 13 | } 14 | 15 | @Override 16 | public void run() { 17 | float lastSample = tpsHistoryTask.getLastSample(); 18 | if (lastSample > 0 && lastSample < 50) { 19 | storage.saveTps(lastSample); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/github/games647/lagmonitor/storage/WorldData.java: -------------------------------------------------------------------------------- 1 | package com.github.games647.lagmonitor.storage; 2 | 3 | import org.bukkit.Chunk; 4 | import org.bukkit.World; 5 | 6 | public class WorldData { 7 | 8 | private final String worldName; 9 | private final int loadedChunks; 10 | private final int tileEntities; 11 | private final int entities; 12 | 13 | private int worldSize; 14 | private int rowId; 15 | 16 | public static WorldData fromWorld(World world) { 17 | String worldName = world.getName(); 18 | int tileEntities = 0; 19 | for (Chunk loadedChunk : world.getLoadedChunks()) { 20 | tileEntities += loadedChunk.getTileEntities().length; 21 | } 22 | 23 | int entities = world.getEntities().size(); 24 | int chunks = world.getLoadedChunks().length; 25 | 26 | return new WorldData(worldName, chunks, tileEntities, entities); 27 | } 28 | 29 | public WorldData(String worldName, int loadedChunks, int tileEntities, int entities) { 30 | this.worldName = worldName; 31 | this.loadedChunks = loadedChunks; 32 | this.tileEntities = tileEntities; 33 | this.entities = entities; 34 | } 35 | 36 | public String getWorldName() { 37 | return worldName; 38 | } 39 | 40 | public int getLoadedChunks() { 41 | return loadedChunks; 42 | } 43 | 44 | public int getTileEntities() { 45 | return tileEntities; 46 | } 47 | 48 | public int getEntities() { 49 | return entities; 50 | } 51 | 52 | public int getWorldSize() { 53 | return worldSize; 54 | } 55 | 56 | public void setWorldSize(int worldSize) { 57 | this.worldSize = worldSize; 58 | } 59 | 60 | public int getRowId() { 61 | return rowId; 62 | } 63 | 64 | public void setRowId(int rowId) { 65 | this.rowId = rowId; 66 | } 67 | 68 | @Override 69 | public String toString() { 70 | return this.getClass().getSimpleName() + '{' + 71 | "worldName=" + worldName 72 | + ", loadedChunks=" + loadedChunks 73 | + ", tileEntities=" + tileEntities 74 | + ", entities=" + entities 75 | + ", worldSize=" + worldSize 76 | + ", rowId=" + rowId 77 | + '}'; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/main/java/com/github/games647/lagmonitor/task/IODetectorTask.java: -------------------------------------------------------------------------------- 1 | package com.github.games647.lagmonitor.task; 2 | 3 | import com.github.games647.lagmonitor.threading.BlockingActionManager; 4 | 5 | import java.lang.Thread.State; 6 | import java.util.TimerTask; 7 | 8 | public class IODetectorTask extends TimerTask { 9 | 10 | private final BlockingActionManager actionManager; 11 | private final Thread mainThread; 12 | 13 | public IODetectorTask(BlockingActionManager actionManager, Thread mainThread) { 14 | this.actionManager = actionManager; 15 | this.mainThread = mainThread; 16 | } 17 | 18 | @Override 19 | public void run() { 20 | //According to this post the thread is still in Runnable although it's waiting for 21 | //file/http resources 22 | //https://stackoverflow.com/questions/20795295/why-jstack-out-says-thread-state-is-runnable-while-socketread 23 | if (mainThread.getState() == State.RUNNABLE) { 24 | //Based on this post we have to check the top element of the stack 25 | //https://stackoverflow.com/questions/20891386/how-to-detect-thread-being-blocked-by-io 26 | StackTraceElement[] stackTrace = mainThread.getStackTrace(); 27 | StackTraceElement topElement = stackTrace[stackTrace.length - 1]; 28 | if (topElement.isNativeMethod()) { 29 | //Socket/SQL (connect) - java.net.DualStackPlainSocketImpl.connect0 30 | //Socket/SQL (read) - java.net.SocketInputStream.socketRead0 31 | //Socket/SQL (write) - java.net.SocketOutputStream.socketWrite0 32 | if (isElementEqual(topElement, "java.net.DualStackPlainSocketImpl", "connect0") 33 | || isElementEqual(topElement, "java.net.SocketInputStream", "socketRead0") 34 | || isElementEqual(topElement, "java.net.SocketOutputStream", "socketWrite0")) { 35 | actionManager.logCurrentStack("Server is performing {1} on the main thread. " 36 | + "Properly caused by {0}", "java.net.SocketStream"); 37 | } //File (in) - java.io.FileInputStream.readBytes 38 | //File (out) - java.io.FileOutputStream.writeBytes 39 | else if (isElementEqual(topElement, "java.io.FileInputStream", "readBytes") 40 | || isElementEqual(topElement, "java.io.FileOutputStream", "writeBytes")) { 41 | actionManager.logCurrentStack("Server is performing {1} on the main thread. " + 42 | "Properly caused by {0}", "java.io.FileStream"); 43 | } 44 | } 45 | } 46 | } 47 | 48 | private boolean isElementEqual(StackTraceElement traceElement, String className, String methodName) { 49 | return traceElement.getClassName().equals(className) && traceElement.getMethodName().equals(methodName); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/com/github/games647/lagmonitor/task/MonitorTask.java: -------------------------------------------------------------------------------- 1 | package com.github.games647.lagmonitor.task; 2 | 3 | import com.github.games647.lagmonitor.MethodMeasurement; 4 | import com.github.games647.lagmonitor.command.MonitorCommand; 5 | import com.google.common.net.UrlEscapers; 6 | import com.google.gson.Gson; 7 | import com.google.gson.JsonObject; 8 | 9 | import java.io.BufferedReader; 10 | import java.io.BufferedWriter; 11 | import java.io.IOException; 12 | import java.io.InputStreamReader; 13 | import java.io.OutputStreamWriter; 14 | import java.io.Reader; 15 | import java.lang.management.ManagementFactory; 16 | import java.lang.management.ThreadInfo; 17 | import java.lang.management.ThreadMXBean; 18 | import java.net.HttpURLConnection; 19 | import java.net.URL; 20 | import java.nio.charset.StandardCharsets; 21 | import java.util.TimerTask; 22 | import java.util.logging.Level; 23 | import java.util.logging.Logger; 24 | 25 | /** 26 | * Based on the project https://github.com/sk89q/WarmRoast by sk89q 27 | */ 28 | public class MonitorTask extends TimerTask { 29 | 30 | private static final String PASTE_URL = "https://paste.enginehub.org/paste"; 31 | private static final int MAX_DEPTH = 25; 32 | 33 | private final ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean(); 34 | private final Logger logger; 35 | private final long threadId; 36 | 37 | private MethodMeasurement rootNode; 38 | private int samples; 39 | 40 | public MonitorTask(Logger logger, long threadId) { 41 | this.logger = logger; 42 | this.threadId = threadId; 43 | } 44 | 45 | public synchronized MethodMeasurement getRootSample() { 46 | return rootNode; 47 | } 48 | 49 | public synchronized int getSamples() { 50 | return samples; 51 | } 52 | 53 | @Override 54 | public void run() { 55 | ThreadInfo threadInfo = threadMXBean.getThreadInfo(threadId, MAX_DEPTH); 56 | StackTraceElement[] stackTrace = threadInfo.getStackTrace(); 57 | if (stackTrace.length > 0) { 58 | StackTraceElement rootElement = stackTrace[stackTrace.length - 1]; 59 | synchronized (this) { 60 | samples++; 61 | 62 | if (rootNode == null) { 63 | String rootClass = rootElement.getClassName(); 64 | String rootMethod = rootElement.getMethodName(); 65 | 66 | String id = rootClass + '.' + rootMethod; 67 | rootNode = new MethodMeasurement(id, rootClass, rootMethod); 68 | } 69 | 70 | rootNode.onMeasurement(stackTrace, 0, MonitorCommand.SAMPLE_INTERVAL); 71 | } 72 | } 73 | } 74 | 75 | public String paste() { 76 | try { 77 | HttpURLConnection httpConnection = (HttpURLConnection) new URL(PASTE_URL).openConnection(); 78 | httpConnection.setRequestMethod("POST"); 79 | httpConnection.setDoOutput(true); 80 | httpConnection.setDoInput(true); 81 | 82 | try (BufferedWriter writer = new BufferedWriter( 83 | new OutputStreamWriter(httpConnection.getOutputStream(), StandardCharsets.UTF_8)) 84 | ) { 85 | writer.write("content=" + UrlEscapers.urlPathSegmentEscaper().escape(toString())); 86 | writer.write("&from=" + logger.getName()); 87 | } 88 | 89 | JsonObject object; 90 | try (Reader reader = new BufferedReader( 91 | new InputStreamReader(httpConnection.getInputStream(), StandardCharsets.UTF_8)) 92 | ) { 93 | object = new Gson().fromJson(reader, JsonObject.class); 94 | } 95 | 96 | if (object.has("url")) { 97 | return object.get("url").getAsString(); 98 | } 99 | 100 | logger.log(Level.INFO, "Failed to parse url from {0}", object); 101 | } catch (IOException ex) { 102 | logger.log(Level.SEVERE, "Failed to upload monitoring data", ex); 103 | } 104 | 105 | return null; 106 | } 107 | 108 | @Override 109 | public String toString() { 110 | ThreadInfo threadInfo = threadMXBean.getThreadInfo(threadId, MAX_DEPTH); 111 | 112 | StringBuilder builder = new StringBuilder(); 113 | builder.append(threadInfo.getThreadName()); 114 | builder.append(' '); 115 | 116 | synchronized (this) { 117 | builder.append(rootNode.getTotalTime()).append("ms"); 118 | builder.append('\n'); 119 | 120 | rootNode.writeString(builder, 1); 121 | } 122 | 123 | return builder.toString(); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/main/java/com/github/games647/lagmonitor/task/PingManager.java: -------------------------------------------------------------------------------- 1 | package com.github.games647.lagmonitor.task; 2 | 3 | import com.github.games647.lagmonitor.ping.PaperPing; 4 | import com.github.games647.lagmonitor.ping.PingFetcher; 5 | import com.github.games647.lagmonitor.ping.ReflectionPing; 6 | import com.github.games647.lagmonitor.ping.SpigotPing; 7 | import com.github.games647.lagmonitor.util.RollingOverHistory; 8 | import com.google.common.collect.Lists; 9 | 10 | import java.lang.reflect.InvocationTargetException; 11 | import java.util.HashMap; 12 | import java.util.List; 13 | import java.util.Map; 14 | 15 | import org.bukkit.Bukkit; 16 | import org.bukkit.entity.Player; 17 | import org.bukkit.event.EventHandler; 18 | import org.bukkit.event.Listener; 19 | import org.bukkit.event.player.PlayerJoinEvent; 20 | import org.bukkit.event.player.PlayerQuitEvent; 21 | import org.bukkit.plugin.Plugin; 22 | 23 | public class PingManager implements Runnable, Listener { 24 | 25 | //the server is pinging the client every 40 Ticks (2 sec) - so check it then 26 | //https://github.com/bergerkiller/CraftSource/blob/master/net.minecraft.server/PlayerConnection.java#L178 27 | public static final int PING_INTERVAL = 2 * 20; 28 | private static final int SAMPLE_SIZE = 5; 29 | 30 | private final Map playerHistory = new HashMap<>(); 31 | private final Plugin plugin; 32 | private final PingFetcher pingFetcher; 33 | 34 | public PingManager(Plugin plugin) throws ReflectiveOperationException { 35 | this.pingFetcher = initializePingFetchur(); 36 | this.plugin = plugin; 37 | } 38 | 39 | private PingFetcher initializePingFetchur() 40 | throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException { 41 | List> fetchurs = Lists.newArrayList( 42 | SpigotPing.class, PaperPing.class, ReflectionPing.class 43 | ); 44 | for (Class fetchurClass : fetchurs) { 45 | PingFetcher fetchur = fetchurClass.getDeclaredConstructor().newInstance(); 46 | if (fetchur.isAvailable()) 47 | return fetchur; 48 | } 49 | 50 | throw new NoSuchMethodException("No valid ping fetcher found"); 51 | } 52 | 53 | @Override 54 | public void run() { 55 | playerHistory.forEach((playerName, history) -> { 56 | Player player = Bukkit.getPlayerExact(playerName); 57 | if (player != null) { 58 | int ping = pingFetcher.getPing(player); 59 | history.add(ping); 60 | } 61 | }); 62 | } 63 | 64 | public RollingOverHistory getHistory(String playerName) { 65 | return playerHistory.get(playerName); 66 | } 67 | 68 | public void addPlayer(Player player) { 69 | int ping = pingFetcher.getPing(player); 70 | playerHistory.put(player.getName(), new RollingOverHistory(SAMPLE_SIZE, ping)); 71 | } 72 | 73 | public void removePlayer(Player player) { 74 | playerHistory.remove(player.getName()); 75 | } 76 | 77 | @EventHandler 78 | public void onPlayerJoin(PlayerJoinEvent joinEvent) { 79 | Player player = joinEvent.getPlayer(); 80 | Bukkit.getScheduler().runTaskLater(plugin, () -> { 81 | if (player.isOnline()) { 82 | addPlayer(player); 83 | } 84 | }, PING_INTERVAL); 85 | } 86 | 87 | @EventHandler 88 | public void onPlayerQuit(PlayerQuitEvent quitEvent) { 89 | removePlayer(quitEvent.getPlayer()); 90 | } 91 | 92 | public void clear() { 93 | playerHistory.clear(); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/main/java/com/github/games647/lagmonitor/task/TPSHistoryTask.java: -------------------------------------------------------------------------------- 1 | package com.github.games647.lagmonitor.task; 2 | 3 | import com.github.games647.lagmonitor.util.RollingOverHistory; 4 | 5 | import java.util.concurrent.TimeUnit; 6 | 7 | public class TPSHistoryTask implements Runnable { 8 | 9 | public static final int RUN_INTERVAL = 20; 10 | private static final int ONE_MINUTE = (int) TimeUnit.MINUTES.toSeconds(1); 11 | 12 | private final RollingOverHistory minuteSample = new RollingOverHistory(ONE_MINUTE, 20.0F); 13 | private final RollingOverHistory quarterSample = new RollingOverHistory(ONE_MINUTE * 15, 20.0F); 14 | private final RollingOverHistory halfHourSample = new RollingOverHistory(ONE_MINUTE * 30, 20.0F); 15 | 16 | //the last time we updated the ticks 17 | private long lastCheck = System.nanoTime(); 18 | 19 | public RollingOverHistory getMinuteSample() { 20 | return minuteSample; 21 | } 22 | 23 | public RollingOverHistory getQuarterSample() { 24 | return quarterSample; 25 | } 26 | 27 | public RollingOverHistory getHalfHourSample() { 28 | return halfHourSample; 29 | } 30 | 31 | public float getLastSample() { 32 | synchronized (this) { 33 | int lastPos = minuteSample.getCurrentPosition(); 34 | return minuteSample.getSamples()[lastPos]; 35 | } 36 | } 37 | 38 | @Override 39 | public void run() { 40 | //nanoTime is more accurate 41 | long currentTime = System.nanoTime(); 42 | long timeSpent = currentTime - lastCheck; 43 | //update the last check 44 | lastCheck = currentTime; 45 | 46 | //how many ticks passed since the last check * 1000 to convert to seconds 47 | float tps = 1 * 20 * 1000.0F / (timeSpent / (1000 * 1000)); 48 | if (tps >= 0.0F && tps < 25.0F) { 49 | //Prevent all invalid values 50 | synchronized (this) { 51 | minuteSample.add(tps); 52 | quarterSample.add(tps); 53 | halfHourSample.add(tps); 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/com/github/games647/lagmonitor/threading/BlockingActionManager.java: -------------------------------------------------------------------------------- 1 | package com.github.games647.lagmonitor.threading; 2 | 3 | import com.google.common.collect.Maps; 4 | import com.google.common.collect.Sets; 5 | 6 | import java.util.Map; 7 | import java.util.Set; 8 | import java.util.logging.Level; 9 | 10 | import org.bukkit.Bukkit; 11 | import org.bukkit.event.Listener; 12 | import org.bukkit.plugin.Plugin; 13 | import org.bukkit.plugin.java.JavaPlugin; 14 | 15 | public class BlockingActionManager implements Listener { 16 | 17 | //feel free to improve the wording of this: 18 | private static final String THREAD_SAFETY_NOTICE = "As threads **can** run concurrently or in parallel " + 19 | "shared data access has to be synchronized (thread-safety) in order to prevent " + 20 | "unexpected behavior or crashes. "; 21 | 22 | private static final String SAFETY_METHODS = "You can guarantee thread-safety by " + 23 | "running the data access always on the same thread, using atomic operations, " + 24 | "locks (ex: a synchronized block), immutable objects, thread local data " + 25 | "or something similar. "; 26 | 27 | private static final String COMMON_SAFE = "Common things that are thread-safe: Logging, Bukkit Scheduler, " + 28 | "Concurrent collections (ex: ConcurrentHashMap or Collections.synchronized*), ... "; 29 | 30 | private static final String BLOCKING_ACTION_MESSAGE = "Plugin {0} is performing a blocking I/O operation ({1}) " + 31 | "on the main thread. " + 32 | "This could affect the server performance, because the thread pauses until it gets the response. " + 33 | "Such operations should be performed asynchronous from the main thread. " + 34 | "Besides gameplay performance it could also improve startup time. " + 35 | "Keep in mind to keep the code thread-safe. "; 36 | 37 | private final Plugin plugin; 38 | 39 | private final Set violations = Sets.newConcurrentHashSet(); 40 | private final Set violatedPlugins = Sets.newConcurrentHashSet(); 41 | 42 | public BlockingActionManager(Plugin plugin) { 43 | this.plugin = plugin; 44 | } 45 | 46 | public void checkBlockingAction(String event) { 47 | if (!Bukkit.isPrimaryThread()) { 48 | return; 49 | } 50 | 51 | logCurrentStack(BLOCKING_ACTION_MESSAGE, event); 52 | } 53 | 54 | public void checkThreadSafety(String eventName) { 55 | if (Bukkit.isPrimaryThread()) { 56 | return; 57 | } 58 | 59 | logCurrentStack("Plugin {0} triggered an synchronous event {1} from an asynchronous Thread. " 60 | + THREAD_SAFETY_NOTICE 61 | + "Use runTask* (no Async*), scheduleSync* or callSyncMethod to run on the main thread.", eventName); 62 | } 63 | 64 | public void logCurrentStack(String format, String eventName) { 65 | IllegalAccessException stackTraceCreator = new IllegalAccessException(); 66 | StackTraceElement[] stackTrace = stackTraceCreator.getStackTrace(); 67 | 68 | Map.Entry foundPlugin = findPlugin(stackTrace); 69 | 70 | PluginViolation violation = new PluginViolation(eventName); 71 | if (foundPlugin != null) { 72 | String pluginName = foundPlugin.getKey(); 73 | violation = new PluginViolation(pluginName, foundPlugin.getValue(), eventName); 74 | if (!violatedPlugins.add(violation.getPluginName()) && plugin.getConfig().getBoolean("oncePerPlugin")) { 75 | return; 76 | } 77 | } 78 | 79 | if (!violations.add(violation)) { 80 | return; 81 | } 82 | 83 | plugin.getLogger().log(Level.WARNING, format + "Report it to the plugin author" 84 | , new Object[]{violation.getPluginName(), eventName}); 85 | 86 | if (plugin.getConfig().getBoolean("hideStacktrace")) { 87 | plugin.getLogger().log(Level.WARNING, "Source: {0}, method {1}, line {2}" 88 | , new Object[]{violation.getSourceFile(), violation.getMethodName(), violation.getLineNumber()}); 89 | } else { 90 | plugin.getLogger().log(Level.WARNING, "The following exception is not an error. " + 91 | "It's a hint for the plugin developers to find the source. " + 92 | plugin.getName() + " doesn't prevent this action. It just warns you about it. ", stackTraceCreator); 93 | } 94 | } 95 | 96 | public Map.Entry findPlugin(StackTraceElement[] stacktrace) { 97 | boolean skipping = true; 98 | for (StackTraceElement elem : stacktrace) { 99 | try { 100 | Class clazz = Class.forName(elem.getClassName()); 101 | if (clazz.getName().endsWith("VanillaCommandWrapper")) { 102 | //explicit use getName instead of SimpleName because getSimpleBinaryName causes a 103 | //StringIndexOutOfBoundsException for obfuscated plugins 104 | return Maps.immutableEntry("Vanilla", elem); 105 | } 106 | 107 | Plugin plugin; 108 | try { 109 | plugin = JavaPlugin.getProvidingPlugin(clazz); 110 | if (plugin == this.plugin) { 111 | continue; 112 | } 113 | 114 | return Maps.immutableEntry(plugin.getName(), elem); 115 | } catch (IllegalArgumentException illegalArgumentEx) { 116 | //ignore 117 | } 118 | } catch (ClassNotFoundException ex) { 119 | //if this class cannot be loaded then it could be something native so we ignore it 120 | } 121 | } 122 | 123 | return null; 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/main/java/com/github/games647/lagmonitor/threading/BlockingSecurityManager.java: -------------------------------------------------------------------------------- 1 | package com.github.games647.lagmonitor.threading; 2 | 3 | import com.google.common.collect.ImmutableSet; 4 | 5 | import java.io.FilePermission; 6 | import java.security.Permission; 7 | import java.util.Set; 8 | 9 | public class BlockingSecurityManager extends SecurityManager implements Injectable { 10 | 11 | private final BlockingActionManager actionManager; 12 | private final Set allowedFiles = ImmutableSet.of(".jar", "session.lock"); 13 | 14 | private SecurityManager delegate; 15 | 16 | public BlockingSecurityManager(BlockingActionManager actionManager) { 17 | this.actionManager = actionManager; 18 | } 19 | 20 | @Override 21 | public void checkPermission(Permission perm, Object context) { 22 | if (delegate != null) { 23 | delegate.checkPermission(perm, context); 24 | } 25 | 26 | checkMainThreadOperation(perm); 27 | } 28 | 29 | @Override 30 | public void checkPermission(Permission perm) { 31 | if (delegate != null) { 32 | delegate.checkPermission(perm); 33 | } 34 | 35 | checkMainThreadOperation(perm); 36 | } 37 | 38 | private void checkMainThreadOperation(Permission perm) { 39 | if (isBlockingAction(perm)) { 40 | actionManager.checkBlockingAction("Permission: " + perm.getName()); 41 | } 42 | } 43 | 44 | private boolean isBlockingAction(Permission permission) { 45 | String actions = permission.getActions(); 46 | return permission instanceof FilePermission 47 | && actions.contains("read") 48 | && allowedFiles.stream().noneMatch(ignored -> permission.getName().contains(ignored)); 49 | } 50 | 51 | @Override 52 | public void inject() { 53 | SecurityManager oldSecurityManager = System.getSecurityManager(); 54 | if (oldSecurityManager != this) { 55 | this.delegate = oldSecurityManager; 56 | System.setSecurityManager(this); 57 | } 58 | } 59 | 60 | @Override 61 | public void restore() { 62 | if (System.getSecurityManager() == this) { 63 | System.setSecurityManager(delegate); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/com/github/games647/lagmonitor/threading/Injectable.java: -------------------------------------------------------------------------------- 1 | package com.github.games647.lagmonitor.threading; 2 | 3 | public interface Injectable { 4 | 5 | void inject(); 6 | 7 | void restore(); 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/com/github/games647/lagmonitor/threading/PluginViolation.java: -------------------------------------------------------------------------------- 1 | package com.github.games647.lagmonitor.threading; 2 | 3 | import java.util.Objects; 4 | 5 | public class PluginViolation { 6 | 7 | private final String pluginName; 8 | private final String sourceFile; 9 | private final String methodName; 10 | private final int lineNumber; 11 | 12 | private final String event; 13 | 14 | public PluginViolation(String pluginName, StackTraceElement stackTraceElement, String event) { 15 | this.pluginName = pluginName; 16 | this.sourceFile = stackTraceElement.getFileName(); 17 | this.methodName = stackTraceElement.getMethodName(); 18 | this.lineNumber = stackTraceElement.getLineNumber(); 19 | 20 | this.event = event; 21 | } 22 | 23 | public PluginViolation(String event) { 24 | this.pluginName = "Unknown"; 25 | this.sourceFile = "Unknown"; 26 | this.methodName = "Unknown"; 27 | this.lineNumber = -1; 28 | 29 | this.event = event; 30 | } 31 | 32 | public String getPluginName() { 33 | return pluginName; 34 | } 35 | 36 | public String getSourceFile() { 37 | return sourceFile; 38 | } 39 | 40 | public String getMethodName() { 41 | return methodName; 42 | } 43 | 44 | public int getLineNumber() { 45 | return lineNumber; 46 | } 47 | 48 | public String getEvent() { 49 | return event; 50 | } 51 | 52 | @Override 53 | public int hashCode() { 54 | return Objects.hash(pluginName, sourceFile, methodName); 55 | } 56 | 57 | @Override 58 | public boolean equals(Object obj) { 59 | if (!(obj instanceof PluginViolation)) { 60 | return false; 61 | } 62 | 63 | PluginViolation other = (PluginViolation) obj; 64 | return Objects.equals(pluginName, other.pluginName) 65 | && Objects.equals(sourceFile, other.sourceFile) 66 | && Objects.equals(methodName, other.methodName); 67 | } 68 | 69 | @Override 70 | public String toString() { 71 | return this.getClass().getSimpleName() + '{' + 72 | "pluginName='" + pluginName + '\'' + 73 | ", sourceFile='" + sourceFile + '\'' + 74 | ", methodName='" + methodName + '\'' + 75 | ", lineNumber=" + lineNumber + 76 | ", event='" + event + '\'' + 77 | '}'; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/main/java/com/github/games647/lagmonitor/traffic/CleanUpTask.java: -------------------------------------------------------------------------------- 1 | package com.github.games647.lagmonitor.traffic; 2 | 3 | import io.netty.channel.ChannelInboundHandlerAdapter; 4 | import io.netty.channel.ChannelPipeline; 5 | 6 | import java.util.NoSuchElementException; 7 | 8 | public class CleanUpTask implements Runnable { 9 | 10 | private final ChannelPipeline pipeline; 11 | private final ChannelInboundHandlerAdapter serverChannelHandler; 12 | 13 | public CleanUpTask(ChannelPipeline pipeline, ChannelInboundHandlerAdapter serverChannelHandler) { 14 | this.pipeline = pipeline; 15 | this.serverChannelHandler = serverChannelHandler; 16 | } 17 | 18 | @Override 19 | public void run() { 20 | try { 21 | pipeline.remove(serverChannelHandler); 22 | } catch (NoSuchElementException e) { 23 | // That's fine 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/github/games647/lagmonitor/traffic/TinyProtocol.java: -------------------------------------------------------------------------------- 1 | package com.github.games647.lagmonitor.traffic; 2 | 3 | import com.github.games647.lagmonitor.traffic.Reflection.FieldAccessor; 4 | 5 | import io.netty.channel.Channel; 6 | import io.netty.channel.ChannelDuplexHandler; 7 | import io.netty.channel.ChannelFuture; 8 | import io.netty.channel.ChannelHandlerContext; 9 | import io.netty.channel.ChannelInboundHandlerAdapter; 10 | import io.netty.channel.ChannelPipeline; 11 | import io.netty.channel.ChannelPromise; 12 | 13 | import java.util.ArrayList; 14 | import java.util.Collection; 15 | import java.util.List; 16 | 17 | import org.bukkit.Bukkit; 18 | import org.bukkit.plugin.Plugin; 19 | 20 | /** 21 | * This is modified version of TinyProtocol from the ProtocolLib authors (dmulloy2 and aadnk). The not relevant things 22 | * are removed like player channel injection and the serverChannelHandler is modified so we can read the raw input of 23 | * the incoming and outgoing packets 24 | * 25 | * Original can be found here: 26 | * https://github.com/dmulloy2/ProtocolLib/blob/master/modules/TinyProtocol/src/main/java/com/comphenix/tinyprotocol/TinyProtocol.java 27 | */ 28 | public abstract class TinyProtocol { 29 | 30 | // Looking up ServerConnection 31 | private static final Class SERVER_CLASS = (Class) Reflection.getMinecraftClass("MinecraftServer", "MinecraftServer"); 32 | private static final Class CONNECTION_CLASS = (Class) Reflection.getMinecraftClass("ServerConnection", "network.ServerConnection"); 33 | private static final Reflection.MethodInvoker GET_SERVER = Reflection.getMethod("{obc}.CraftServer", "getServer"); 34 | private static final FieldAccessor GET_CONNECTION = Reflection.getField(SERVER_CLASS, CONNECTION_CLASS, 0); 35 | 36 | // Injected channel handlers 37 | private final Collection serverChannels = new ArrayList<>(); 38 | private ChannelInboundHandlerAdapter serverChannelHandler; 39 | 40 | private volatile boolean closed; 41 | protected final Plugin plugin; 42 | 43 | /** 44 | * Construct a new instance of TinyProtocol, and start intercepting packets for all connected clients and future 45 | * clients. 46 | *

47 | * You can construct multiple instances per plugin. 48 | * 49 | * @param plugin - the plugin. 50 | */ 51 | public TinyProtocol(final Plugin plugin) { 52 | this.plugin = plugin; 53 | 54 | try { 55 | registerChannelHandler(); 56 | } catch (IllegalArgumentException illegalArgumentException) { 57 | // Damn you, late bind 58 | plugin.getLogger().info("[TinyProtocol] Delaying server channel injection due to late bind."); 59 | 60 | // Damn you, late bind 61 | Bukkit.getScheduler().runTask(plugin, () -> { 62 | registerChannelHandler(); 63 | plugin.getLogger().info("[TinyProtocol] Late bind injection successful."); 64 | }); 65 | } 66 | } 67 | 68 | private void createServerChannelHandler() { 69 | serverChannelHandler = new ChannelInboundHandlerAdapter() { 70 | 71 | @Override 72 | public void channelRead(ChannelHandlerContext ctx, Object msg) { 73 | Channel channel = (Channel) msg; 74 | 75 | channel.pipeline().addLast(new ChannelDuplexHandler() { 76 | @Override 77 | public void channelRead(ChannelHandlerContext handlerContext, Object object) throws Exception { 78 | onChannelRead(handlerContext, object); 79 | super.channelRead(handlerContext, object); 80 | } 81 | 82 | @Override 83 | public void write(ChannelHandlerContext handlerContext, Object object, ChannelPromise promise) 84 | throws Exception { 85 | onChannelWrite(handlerContext, object, promise); 86 | super.write(handlerContext, object, promise); 87 | } 88 | }); 89 | 90 | ctx.fireChannelRead(msg); 91 | } 92 | }; 93 | } 94 | 95 | public abstract void onChannelRead(ChannelHandlerContext handlerContext, Object object); 96 | 97 | public abstract void onChannelWrite(ChannelHandlerContext handlerContext, Object object, ChannelPromise promise); 98 | 99 | @SuppressWarnings("unchecked") 100 | private void registerChannelHandler() { 101 | Object mcServer = GET_SERVER.invoke(Bukkit.getServer()); 102 | Object serverConnection = GET_CONNECTION.get(mcServer); 103 | 104 | createServerChannelHandler(); 105 | 106 | // Find the correct list, or implicitly throw an exception 107 | boolean looking = true; 108 | for (int i = 0; looking; i++) { 109 | List list = Reflection.getField(serverConnection.getClass(), List.class, i).get(serverConnection); 110 | 111 | for (Object item : list) { 112 | if (!(item instanceof ChannelFuture)) { 113 | break; 114 | } 115 | 116 | // Channel future that contains the server connection 117 | Channel serverChannel = ((ChannelFuture) item).channel(); 118 | 119 | serverChannels.add(serverChannel); 120 | serverChannel.pipeline().addFirst(serverChannelHandler); 121 | looking = false; 122 | } 123 | } 124 | } 125 | 126 | private void unregisterChannelHandler() { 127 | if (serverChannelHandler == null) { 128 | return; 129 | } 130 | 131 | serverChannels.forEach(serverChannel -> { 132 | // Remove channel handler 133 | ChannelPipeline pipeline = serverChannel.pipeline(); 134 | serverChannel.eventLoop().execute(new CleanUpTask(pipeline, serverChannelHandler)); 135 | }); 136 | } 137 | 138 | /** 139 | * Cease listening for packets. This is called automatically when your plugin is disabled. 140 | */ 141 | public final void close() { 142 | if (!closed) { 143 | closed = true; 144 | 145 | unregisterChannelHandler(); 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/main/java/com/github/games647/lagmonitor/traffic/TrafficReader.java: -------------------------------------------------------------------------------- 1 | package com.github.games647.lagmonitor.traffic; 2 | 3 | import io.netty.buffer.ByteBuf; 4 | import io.netty.buffer.ByteBufHolder; 5 | import io.netty.channel.ChannelHandlerContext; 6 | import io.netty.channel.ChannelPromise; 7 | 8 | import java.util.concurrent.atomic.LongAdder; 9 | 10 | import org.bukkit.plugin.Plugin; 11 | 12 | public class TrafficReader extends TinyProtocol { 13 | 14 | private final LongAdder incomingBytes = new LongAdder(); 15 | private final LongAdder outgoingBytes = new LongAdder(); 16 | 17 | public TrafficReader(Plugin plugin) { 18 | super(plugin); 19 | } 20 | 21 | public LongAdder getIncomingBytes() { 22 | return incomingBytes; 23 | } 24 | 25 | public LongAdder getOutgoingBytes() { 26 | return outgoingBytes; 27 | } 28 | 29 | @Override 30 | public void onChannelRead(ChannelHandlerContext handlerContext, Object object) { 31 | onChannel(object, true); 32 | } 33 | 34 | @Override 35 | public void onChannelWrite(ChannelHandlerContext handlerContext, Object object, ChannelPromise promise) { 36 | onChannel(object, false); 37 | } 38 | 39 | private void onChannel(Object object, boolean incoming) { 40 | ByteBuf bytes = null; 41 | if (object instanceof ByteBuf) { 42 | bytes = ((ByteBuf) object); 43 | } else if (object instanceof ByteBufHolder) { 44 | bytes = ((ByteBufHolder) object).content(); 45 | } 46 | 47 | if (bytes != null) { 48 | int readableBytes = bytes.readableBytes(); 49 | if (incoming) { 50 | incomingBytes.add(readableBytes); 51 | } else { 52 | outgoingBytes.add(readableBytes); 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/com/github/games647/lagmonitor/util/JavaVersion.java: -------------------------------------------------------------------------------- 1 | package com.github.games647.lagmonitor.util; 2 | 3 | import com.google.common.collect.ComparisonChain; 4 | 5 | import java.util.Objects; 6 | import java.util.Optional; 7 | import java.util.regex.Matcher; 8 | import java.util.regex.Pattern; 9 | 10 | public class JavaVersion implements Comparable { 11 | 12 | public static final JavaVersion LATEST = new JavaVersion("14.0.1", 14, 0, 1, false); 13 | 14 | private static final Pattern VERSION_PATTERN = Pattern.compile("((1\\.)?(\\d+))(\\.(\\d+))?(\\.(\\d+))?"); 15 | 16 | private final String raw; 17 | 18 | private final int major; 19 | private final int minor; 20 | private final int security; 21 | private final boolean preRelease; 22 | 23 | protected JavaVersion(String raw, int major, int minor, int security, boolean preRelease) { 24 | this.raw = raw; 25 | this.major = major; 26 | this.minor = minor; 27 | this.security = security; 28 | this.preRelease = preRelease; 29 | } 30 | 31 | public JavaVersion(String version) { 32 | raw = version; 33 | preRelease = version.contains("-ea") || version.contains("-internal"); 34 | 35 | Matcher matcher = VERSION_PATTERN.matcher(version); 36 | if (!matcher.find()) { 37 | throw new IllegalStateException("Cannot parse Java version"); 38 | } 39 | 40 | major = Optional.ofNullable(matcher.group(3)).map(Integer::parseInt).orElse(0); 41 | if (major == 8) { 42 | // If you have a better solution feel free to contribute 43 | // Source: https://openjdk.java.net/jeps/223 44 | // Minor releases containing changes beyond security fixes are multiples of 20. Security releases based on 45 | // the previous minor release are odd numbers incremented by five, or by six if necessary in order to keep 46 | // the update number odd. 47 | int update = Integer.parseInt(version.substring(version.indexOf('_') + 1)); 48 | minor = update / 20; 49 | security = update % 20; 50 | } else { 51 | minor = Optional.ofNullable(matcher.group(5)).map(Integer::parseInt).orElse(0); 52 | security = Optional.ofNullable(matcher.group(7)).map(Integer::parseInt).orElse(0); 53 | } 54 | } 55 | 56 | public static JavaVersion detect() { 57 | return new JavaVersion(System.getProperty("java.version")); 58 | } 59 | 60 | public String getRaw() { 61 | return raw; 62 | } 63 | 64 | public int getMajor() { 65 | return major; 66 | } 67 | 68 | public int getMinor() { 69 | return minor; 70 | } 71 | 72 | public int getSecurity() { 73 | return security; 74 | } 75 | 76 | public boolean isPreRelease() { 77 | return preRelease; 78 | } 79 | 80 | public boolean isOutdated() { 81 | return this.compareTo(LATEST) < 0; 82 | } 83 | 84 | @Override 85 | public int compareTo(JavaVersion other) { 86 | return ComparisonChain.start() 87 | .compare(major, other.major) 88 | .compare(minor, other.minor) 89 | .compare(security, other.security) 90 | .compareTrueFirst(preRelease, other.preRelease) 91 | .result(); 92 | } 93 | 94 | @Override 95 | public boolean equals(Object other) { 96 | if (this == other) return true; 97 | if (!(other instanceof JavaVersion)) return false; 98 | JavaVersion that = (JavaVersion) other; 99 | return major == that.major && 100 | minor == that.minor && 101 | security == that.security && 102 | preRelease == that.preRelease; 103 | } 104 | 105 | @Override 106 | public int hashCode() { 107 | return Objects.hash(raw, major, minor, security, preRelease); 108 | } 109 | 110 | @Override 111 | public String toString() { 112 | return this.getClass().getSimpleName() + '{' + 113 | "raw='" + raw + '\'' + 114 | ", major=" + major + 115 | ", minor=" + minor + 116 | ", security=" + security + 117 | ", preRelease=" + preRelease + 118 | '}'; 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/main/java/com/github/games647/lagmonitor/util/LagUtils.java: -------------------------------------------------------------------------------- 1 | package com.github.games647.lagmonitor.util; 2 | 3 | import com.google.common.base.Enums; 4 | 5 | import java.io.IOException; 6 | import java.math.BigDecimal; 7 | import java.math.RoundingMode; 8 | import java.nio.file.Files; 9 | import java.nio.file.Path; 10 | import java.util.logging.Level; 11 | import java.util.logging.Logger; 12 | import java.util.stream.Stream; 13 | 14 | import org.bukkit.Material; 15 | 16 | public class LagUtils { 17 | 18 | private LagUtils() { 19 | } 20 | 21 | public static int byteToMega(long bytes) { 22 | return (int) (bytes / (1024 * 1024)); 23 | } 24 | 25 | public static float round(double number) { 26 | return round(number, 2); 27 | } 28 | 29 | public static float round(double value, int places) { 30 | BigDecimal bd = new BigDecimal(value); 31 | bd = bd.setScale(2, RoundingMode.HALF_UP); 32 | return bd.floatValue(); 33 | } 34 | 35 | /** 36 | * Check if the current server version supports filled maps and MapView.setView methods. 37 | * @return true if supported 38 | */ 39 | public static boolean isFilledMapSupported() { 40 | return Enums.getIfPresent(Material.class, "FILLED_MAP").isPresent(); 41 | } 42 | 43 | public static String readableBytes(long bytes) { 44 | //https://stackoverflow.com/questions/3758606/how-to-convert-byte-size-into-human-readable-format-in-java 45 | int unit = 1024; 46 | if (bytes < unit) { 47 | return bytes + " B"; 48 | } 49 | 50 | int exp = (int) (Math.log(bytes) / Math.log(unit)); 51 | String pre = "kMGTPE".charAt(exp - 1) + "i"; 52 | return String.format("%.2f %sB", bytes / Math.pow(unit, exp), pre); 53 | } 54 | 55 | public static long getFolderSize(Logger logger, Path folder) { 56 | try (Stream walk = Files.walk(folder, 3)) { 57 | return walk 58 | .parallel() 59 | .filter(Files::isRegularFile) 60 | .mapToLong(path -> { 61 | try { 62 | return Files.size(path); 63 | } catch (IOException ioEx) { 64 | return 0; 65 | } 66 | }).sum(); 67 | } catch (IOException ioEx) { 68 | logger.log(Level.INFO, "Cannot walk file tree to calculate folder size", ioEx); 69 | } 70 | 71 | return -1; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/main/java/com/github/games647/lagmonitor/util/RollingOverHistory.java: -------------------------------------------------------------------------------- 1 | package com.github.games647.lagmonitor.util; 2 | 3 | import java.util.Arrays; 4 | 5 | public class RollingOverHistory { 6 | 7 | private final float[] samples; 8 | private float total; 9 | 10 | private int currentPosition; 11 | private int currentSize = 1; 12 | 13 | public RollingOverHistory(int size, float firstValue) { 14 | this.samples = new float[size]; 15 | reset(firstValue); 16 | } 17 | 18 | public void add(float sample) { 19 | currentPosition++; 20 | if (currentPosition >= samples.length) { 21 | //we reached the end - go back to the beginning 22 | currentPosition = 0; 23 | } 24 | 25 | if (currentSize < samples.length) { 26 | //array is not full yet 27 | currentSize++; 28 | } 29 | 30 | //delete the latest sample which wil be overridden 31 | total -= samples[currentPosition]; 32 | 33 | total += sample; 34 | samples[currentPosition] = sample; 35 | } 36 | 37 | public float getAverage() { 38 | return total / currentSize; 39 | } 40 | 41 | public int getCurrentPosition() { 42 | return currentPosition; 43 | } 44 | 45 | public int getCurrentSize() { 46 | return currentSize; 47 | } 48 | 49 | public float getLastSample() { 50 | int lastPos = currentPosition; 51 | if (lastPos < 0) { 52 | lastPos = samples.length - 1; 53 | } 54 | 55 | return samples[lastPos]; 56 | } 57 | 58 | public float[] getSamples() { 59 | return Arrays.copyOf(samples, samples.length); 60 | } 61 | 62 | public void reset(float firstVal) { 63 | samples[0] = firstVal; 64 | total = firstVal; 65 | } 66 | 67 | @Override 68 | public String toString() { 69 | return this.getClass().getSimpleName() + '{' + 70 | "samples=" + Arrays.toString(samples) + 71 | ", total=" + total + 72 | ", currentPosition=" + currentPosition + 73 | ", currentSize=" + currentSize + 74 | '}'; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/services/org.slf4j.spi.SLF4JServiceProvider: -------------------------------------------------------------------------------- 1 | com.github.games647.lagmonitor.logging.ForwardLogService 2 | -------------------------------------------------------------------------------- /src/main/resources/config.yml: -------------------------------------------------------------------------------- 1 | # ${project.name} main config 2 | 3 | # If this option is enabled, this plugin will check for events which should run on the main 4 | # thread. If this not the cause the plugin will throw an exception to inform you. Therefore 5 | # you can detect thread-safety issues which could end up in ConcurrentModificationExceptions 6 | # or other issues. 7 | thread-safety-check: true 8 | 9 | # Check periodically if a server (especially a plugin) is doing block I/O operations on the main thread. 10 | # Operations like SQL-, HTTP-Request, File or Socket-Connections should be performed in a separate thread. 11 | # If this is not the case, the server will wait for the response and therefore causes lags. 12 | # 13 | # This can be useful for plugins that use thread pools, where the connection is initialized in background, but the 14 | # data is retrieved on the main thread (e.g. Hikari SQL database pools). The following options can't detect that. 15 | thread-block-detection: false 16 | 17 | # This does the same as the option above, but it proves better performance. This means it can only check for 18 | # Socket connections -> HTTP, SQL, but not for file operations. 19 | socket-block-detection: true 20 | 21 | # This is a more efficient system as the method above (thread-block detection) 22 | # By setting a new security manager we can receive the operations above as events and don't have 23 | # to check it periodically. 24 | # Warning: this may override the existing security manager which could be set by your hoster 25 | securityMangerBlockingCheck: true 26 | 27 | # If you see something like: "Server is performing a threading socket connection ..." and then a long list with "at .." 28 | # It's properly one of the four features above. The last part is the stacktrace. 29 | # This can help developers where their was running in order to find the source. 30 | # 31 | # If the developer is unreachable and it's too much for you, can deactivate it here. Then you still get the warning 32 | # but you don't see the stacktrace. 33 | hideStacktrace: false 34 | 35 | # Show the warning from above only once per plugin 36 | oncePerPlugin: false 37 | 38 | # By hooking into the network management of Minecraft we can read how many bytes 39 | # the server is receiving or is sending. 40 | traffic-counter: true 41 | 42 | # If you enable this, it will save some monitoring data periodically into a MySQL database 43 | # There you can find lag sources with a history 44 | # And you could create a web interface for monitoring your server 45 | monitor-database: false 46 | 47 | # Database configuration 48 | # Recommended is the use of MariaDB (a better version of MySQL) 49 | host: 127.0.0.1 50 | port: 3306 51 | database: lagmonitor 52 | usessl: false 53 | username: myUser 54 | password: myPassword 55 | tablePrefix: 'lgm_' 56 | 57 | # Interval is in seconds 58 | 59 | # Containing the current TPS and a updated timestamp 60 | tps-save-interval: 300 61 | 62 | # Containing some monitoring information to analyze your log 63 | monitor-save-interval: 900 64 | 65 | # Information about your server which is good to see, but might not be really useful for finding lag sources 66 | # For example: 67 | # * native 68 | # * Minecraft traffic counter 69 | # * Minecraft process specific writes and reads 70 | native-save-interval: 1200 71 | 72 | # A permissions independent way to allow certain commands 73 | # 74 | # Everything starts with 'allow-' and then the command name 75 | # 76 | # You can add as many commands as you want to 77 | # If the command doesn't exist here it won't be allowed at all 78 | # Everyone who has the permission for that command can then use it. 79 | # 80 | # Here an example for the native command: 81 | # allow-native: 82 | # - PlayerName 83 | # - Example 84 | allow-commandname: 85 | - PlayerName 86 | - Example 87 | 88 | 89 | -------------------------------------------------------------------------------- /src/main/resources/create.sql: -------------------------------------------------------------------------------- 1 | # LagMonitor table 2 | # Add Ids to each table, because it would be easier to refer to those entries in a lightweight way 3 | # Example: From a monitoring application 4 | # Alternatively we could also use no primary keys at all for some tables 5 | 6 | CREATE TABLE IF NOT EXISTS `{prefix}tps` 7 | ( 8 | `tps_id` INTEGER UNSIGNED PRIMARY KEY AUTO_INCREMENT, 9 | `tps` FLOAT UNSIGNED NOT NULL, 10 | `updated` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP 11 | ); 12 | 13 | CREATE TABLE IF NOT EXISTS `{prefix}monitor` 14 | ( 15 | `monitor_id` INTEGER UNSIGNED PRIMARY KEY AUTO_INCREMENT, 16 | `process_usage` FLOAT UNSIGNED NOT NULL, 17 | `os_usage` FLOAT UNSIGNED NOT NULL, 18 | `free_ram` MEDIUMINT UNSIGNED NOT NULL, 19 | `free_ram_pct` FLOAT UNSIGNED NOT NULL, 20 | `os_free_ram` MEDIUMINT UNSIGNED NOT NULL, 21 | `os_free_ram_pct` FLOAT UNSIGNED NOT NULL, 22 | `load_avg` FLOAT UNSIGNED NOT NULL, 23 | `updated` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP 24 | ); 25 | 26 | CREATE TABLE IF NOT EXISTS `{prefix}worlds` 27 | ( 28 | `world_id` INTEGER UNSIGNED PRIMARY KEY AUTO_INCREMENT, 29 | `monitor_id` INTEGER UNSIGNED NOT NULL, 30 | `world_name` VARCHAR(255) NOT NULL, 31 | `chunks_loaded` SMALLINT UNSIGNED NOT NULL, 32 | `tile_entities` SMALLINT UNSIGNED NOT NULL, 33 | `world_size` MEDIUMINT UNSIGNED NOT NULL, 34 | `entities` INT UNSIGNED NOT NULL, 35 | FOREIGN KEY (`monitor_id`) REFERENCES `{prefix}monitor` (`monitor_id`) 36 | ); 37 | 38 | CREATE TABLE IF NOT EXISTS `{prefix}players` 39 | ( 40 | `world_id` INTEGER UNSIGNED, 41 | `uuid` CHAR(40) NOT NULL, 42 | `name` VARCHAR(16) NOT NULL, 43 | `ping` SMALLINT UNSIGNED NOT NULL, 44 | PRIMARY KEY (`world_id`, `uuid`), 45 | FOREIGN KEY (`world_id`) REFERENCES `{prefix}worlds` (`world_id`) 46 | ); 47 | 48 | CREATE TABLE IF NOT EXISTS `{prefix}native` 49 | ( 50 | `native_id` INTEGER UNSIGNED PRIMARY KEY AUTO_INCREMENT, 51 | `mc_read` SMALLINT UNSIGNED, 52 | `mc_write` SMALLINT UNSIGNED, 53 | `free_space` INT UNSIGNED, 54 | `free_space_pct` FLOAT UNSIGNED, 55 | `disk_read` SMALLINT UNSIGNED, 56 | `disk_write` SMALLINT UNSIGNED, 57 | `net_read` SMALLINT UNSIGNED, 58 | `net_write` SMALLINT UNSIGNED, 59 | `updated` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP 60 | ); 61 | -------------------------------------------------------------------------------- /src/main/resources/plugin.yml: -------------------------------------------------------------------------------- 1 | # project data for Bukkit in order to register our plugin with all it components 2 | # ${-} are variables from Maven (pom.xml) which will be replaced after the build 3 | name: ${project.name} 4 | version: ${project.version}-${git.commit.id.abbrev} 5 | main: ${project.groupId}.${project.artifactId}.${project.name} 6 | 7 | # meta data for plugin managers 8 | authors: [games647, 'https://github.com/games647/LagMonitor/graphs/contributors'] 9 | description: | 10 | ${project.description} 11 | website: ${project.url} 12 | dev-url: ${project.url} 13 | 14 | # This plugin don't have to be transformed for compatibility with Minecraft >= 1.13 15 | api-version: 1.16 16 | 17 | libraries: 18 | - net.java.dev.jna:jna:5.12.1 19 | 20 | commands: 21 | lagmonitor: 22 | description: 'Gets displays the help page of all lagmonitor commands' 23 | permission: ${project.artifactId}.command.help 24 | ping: 25 | description: 'Gets the ping of the selected player' 26 | usage: '/ [player]' 27 | permission: ${project.artifactId}.command.ping 28 | stacktrace: 29 | description: 'Gets the execution stacktrace of selected thread' 30 | usage: '/ [threadName]' 31 | permission: ${project.artifactId}.command.stacktrace 32 | thread: 33 | description: 'Outputs all running threads with their current state' 34 | usage: '/ [dump]' 35 | aliases: [threads] 36 | permission: ${project.artifactId}.command.thread 37 | tpshistory: 38 | description: 'Outputs the current tps' 39 | aliases: [tps, lag] 40 | permission: ${project.artifactId}.command.tps 41 | mbean: 42 | description: 'Outputs mbeans attributes (java environment information)' 43 | aliases: [bean] 44 | usage: '/ [beanName] [attribute]' 45 | permission: ${project.artifactId}.command.mbean 46 | system: 47 | description: 'Gives you some general information (Minecraft server related)' 48 | permission: ${project.artifactId}.command.system 49 | timing: 50 | description: 'Outputs your server timings ingame' 51 | permission: ${project.artifactId}.command.timing 52 | monitor: 53 | description: 'Monitors the CPU usage of methods' 54 | permission: ${project.artifactId}.command.monitor 55 | usage: '/ [start/stop]' 56 | aliases: [profile, profiler, prof] 57 | graph: 58 | description: 'Gives you visual graph about your server' 59 | usage: '/ [heap/cpu/threads/classes]' 60 | permission: ${project.artifactId}.command.graph 61 | environment: 62 | description: 'Gives you some general information (OS related)' 63 | permission: ${project.artifactId}.command.environment 64 | aliases: [env] 65 | native: 66 | description: 'Gives you information about your Hardware' 67 | permission: ${project.artifactId}.command.native 68 | vm: 69 | description: 'Gives you information about your Hardware' 70 | aliases: [virtualmachine, machine, virtual] 71 | permission: ${project.artifactId}.command.vm 72 | network: 73 | description: 'Gives you information about your Network configuration' 74 | aliases: [net] 75 | permission: ${project.artifactId}.command.network 76 | tasks: 77 | description: 'Information about running and pending tasks' 78 | aliases: [task] 79 | permission: ${project.artifactId}.command.tasks 80 | heap: 81 | description: 'Heap dump about your current memory' 82 | aliases: [ram, memory] 83 | usage: / [dump] 84 | permission: ${project.artifactId}.command.heap 85 | lagpage: 86 | description: 'Pages command for the current pagination session' 87 | usage: '/ ' 88 | jfr: 89 | description: | 90 | 'Manages the Java Flight Recordings of the native Java VM. It gives you much more detailed information 91 | including network communications, file read/write times, detailed heap and thread data, ...' 92 | aliases: [flightrecoder] 93 | usage: '/ ' 94 | permission: ${project.artifactId}.command.jfr 95 | 96 | permissions: 97 | ${project.artifactId}.*: 98 | description: Gives access to all ${project.name} Features 99 | children: 100 | ${project.artifactId}.commands.*: true 101 | 102 | ${project.artifactId}.commands.*: 103 | description: Gives access to all ${project.name} commands 104 | children: 105 | ${project.artifactId}.command.ping: true 106 | ${project.artifactId}.command.ping.other: true 107 | ${project.artifactId}.command.stacktrace: true 108 | ${project.artifactId}.command.thread: true 109 | ${project.artifactId}.command.tps: true 110 | ${project.artifactId}.command.mbean: true 111 | ${project.artifactId}.command.system: true 112 | ${project.artifactId}.command.timing: true 113 | ${project.artifactId}.command.monitor: true 114 | ${project.artifactId}.command.graph: true 115 | ${project.artifactId}.command.native: true 116 | ${project.artifactId}.command.vm: true 117 | ${project.artifactId}.command.network: true 118 | ${project.artifactId}.command.tasks: true 119 | ${project.artifactId}.command.jfr: true 120 | 121 | ${project.artifactId}.command.ping.other: 122 | description: 'Get the ping from other players' 123 | children: 124 | ${project.artifactId}.command.ping: true 125 | -------------------------------------------------------------------------------- /src/test/java/com/github/games647/lagmonitor/LagMonitorTest.java: -------------------------------------------------------------------------------- 1 | package com.github.games647.lagmonitor; 2 | 3 | import java.time.Duration; 4 | 5 | import org.junit.jupiter.api.Test; 6 | 7 | import static org.junit.jupiter.api.Assertions.assertEquals; 8 | 9 | public class LagMonitorTest { 10 | 11 | @Test 12 | public void testEmptyDuration() { 13 | assertEquals("'0' days '0' hours '0' minutes '0' seconds'", LagMonitor.formatDuration(Duration.ZERO)); 14 | } 15 | 16 | @Test 17 | public void testOverYearDuration() { 18 | String expected = "'362' days '0' hours '0' minutes '0' seconds'"; 19 | assertEquals(expected, LagMonitor.formatDuration(Duration.ofDays(362))); 20 | } 21 | 22 | @Test 23 | public void testValidSecondDuration() { 24 | String expected = "'0' days '0' hours '0' minutes '1' seconds'"; 25 | assertEquals(expected, LagMonitor.formatDuration(Duration.ofSeconds(1))); 26 | } 27 | 28 | @Test 29 | public void testOverSecondDuration() { 30 | String expected = "'0' days '0' hours '1' minutes '15' seconds'"; 31 | assertEquals(expected, LagMonitor.formatDuration(Duration.ofSeconds(75))); 32 | } 33 | 34 | @Test 35 | public void testFormattingCombined() { 36 | String expected = "'0' days '0' hours '13' minutes '15' seconds'"; 37 | assertEquals(expected, LagMonitor.formatDuration(Duration.ofSeconds(75).plusMinutes(12))); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/test/java/com/github/games647/lagmonitor/RollingOverHistoryTest.java: -------------------------------------------------------------------------------- 1 | package com.github.games647.lagmonitor; 2 | 3 | import com.github.games647.lagmonitor.util.RollingOverHistory; 4 | 5 | import org.junit.jupiter.api.Test; 6 | 7 | import static org.junit.jupiter.api.Assertions.assertArrayEquals; 8 | import static org.junit.jupiter.api.Assertions.assertEquals; 9 | 10 | public class RollingOverHistoryTest { 11 | 12 | @Test 13 | public void testGetAverage() { 14 | RollingOverHistory history = new RollingOverHistory(4, 1); 15 | 16 | assertEquals(1.0F, history.getAverage()); 17 | history.add(3); 18 | assertEquals(2.0F, history.getAverage()); 19 | history.add(2); 20 | assertEquals(2.0F, history.getAverage()); 21 | history.add(3); 22 | assertEquals(2.25F, history.getAverage()); 23 | } 24 | 25 | @Test 26 | public void testGetCurrentPosition() { 27 | RollingOverHistory history = new RollingOverHistory(2, 1); 28 | 29 | assertEquals(0, history.getCurrentPosition()); 30 | history.add(2); 31 | 32 | assertEquals(1, history.getCurrentPosition()); 33 | history.add(2); 34 | //reached the max size 35 | assertEquals(0, history.getCurrentPosition()); 36 | } 37 | 38 | @Test 39 | public void testGetLastSample() { 40 | RollingOverHistory history = new RollingOverHistory(3, 1); 41 | 42 | assertEquals(1.0, history.getLastSample()); 43 | history.add(2); 44 | assertEquals(2.0, history.getLastSample()); 45 | history.add(3); 46 | assertEquals(3.0, history.getLastSample()); 47 | history.add(2); 48 | assertEquals(2.0, history.getLastSample()); 49 | } 50 | 51 | @Test 52 | public void testGetSamples() { 53 | RollingOverHistory history = new RollingOverHistory(1, 1); 54 | 55 | history.add(2); 56 | assertArrayEquals(new float[]{2.0F}, history.getSamples()); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/test/java/com/github/games647/lagmonitor/listener/BlockingConnectionSelectorTest.java: -------------------------------------------------------------------------------- 1 | package com.github.games647.lagmonitor.listener; 2 | 3 | import com.github.games647.lagmonitor.threading.BlockingActionManager; 4 | 5 | import java.net.URI; 6 | 7 | import org.junit.jupiter.api.BeforeEach; 8 | import org.junit.jupiter.api.Test; 9 | import org.junit.jupiter.api.extension.ExtendWith; 10 | import org.mockito.Mock; 11 | import org.mockito.junit.jupiter.MockitoExtension; 12 | 13 | import static org.mockito.ArgumentMatchers.anyString; 14 | 15 | import static org.mockito.Mockito.times; 16 | import static org.mockito.Mockito.verify; 17 | 18 | @ExtendWith(MockitoExtension.class) 19 | public class BlockingConnectionSelectorTest { 20 | 21 | @Mock 22 | private BlockingActionManager actionManager; 23 | private BlockingConnectionSelector selector; 24 | 25 | @BeforeEach 26 | public void setUp() throws Exception { 27 | this.selector = new BlockingConnectionSelector(actionManager); 28 | } 29 | 30 | @Test 31 | public void testHttp() throws Exception { 32 | selector.select(URI.create("https://spigotmc.org")); 33 | verify(actionManager, times(1)).checkBlockingAction(anyString()); 34 | } 35 | 36 | @Test 37 | public void testDuplicateHttp() throws Exception { 38 | //http creates to proxy selector events one for http address and one for the socket one 39 | //the second one should be ignored 40 | selector.select(URI.create("socket://api.mojang.com:443")); 41 | verify(actionManager, times(0)).checkBlockingAction(anyString()); 42 | } 43 | 44 | @Test 45 | public void testBlockingSocket() throws Exception { 46 | selector.select(URI.create("socket://api.mojang.com:50")); 47 | verify(actionManager, times(1)).checkBlockingAction(anyString()); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/test/java/com/github/games647/lagmonitor/util/JavaVersionTest.java: -------------------------------------------------------------------------------- 1 | package com.github.games647.lagmonitor.util; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static org.junit.jupiter.api.Assertions.assertEquals; 6 | import static org.junit.jupiter.api.Assertions.assertFalse; 7 | import static org.junit.jupiter.api.Assertions.assertNotNull; 8 | import static org.junit.jupiter.api.Assertions.assertTrue; 9 | 10 | public class JavaVersionTest { 11 | 12 | @Test 13 | public void detectDeveloperVersion() { 14 | assertNotNull(JavaVersion.detect()); 15 | } 16 | 17 | @Test 18 | public void parseJava8() { 19 | JavaVersion version = new JavaVersion("1.8.0_161"); 20 | assertEquals(8, version.getMajor()); 21 | assertEquals(8, version.getMinor()); 22 | assertEquals(1, version.getSecurity()); 23 | assertTrue(version.isOutdated()); 24 | } 25 | 26 | @Test 27 | public void parseJava9() { 28 | JavaVersion version = new JavaVersion("9.0.4"); 29 | assertEquals(9, version.getMajor()); 30 | assertEquals(0, version.getMinor()); 31 | assertEquals(4, version.getSecurity()); 32 | assertTrue(version.isOutdated()); 33 | } 34 | 35 | @Test 36 | public void parseJava9EarlyAccess() { 37 | JavaVersion version = new JavaVersion("9-ea"); 38 | assertEquals(9, version.getMajor()); 39 | assertEquals(0, version.getMinor()); 40 | assertEquals(0, version.getSecurity()); 41 | assertTrue(version.isPreRelease()); 42 | assertTrue(version.isOutdated()); 43 | } 44 | 45 | @Test 46 | public void parseJava9WithVendorSuffix() { 47 | JavaVersion version = new JavaVersion("9-Ubuntu"); 48 | assertEquals(9, version.getMajor()); 49 | assertEquals(0, version.getMinor()); 50 | assertEquals(0, version.getSecurity()); 51 | assertTrue(version.isOutdated()); 52 | } 53 | 54 | @Test 55 | public void parseJava14() { 56 | JavaVersion version = new JavaVersion("14.0.1"); 57 | assertEquals(14, version.getMajor()); 58 | assertEquals(0, version.getMinor()); 59 | assertEquals(1, version.getSecurity()); 60 | assertFalse(version.isOutdated()); 61 | } 62 | 63 | @Test 64 | public void parseJava10Internal() { 65 | JavaVersion version = new JavaVersion("10-internal"); 66 | assertEquals(10, version.getMajor()); 67 | assertEquals(0, version.getMinor()); 68 | assertEquals(0, version.getSecurity()); 69 | assertTrue(version.isPreRelease()); 70 | assertTrue(version.isOutdated()); 71 | } 72 | 73 | @Test 74 | public void comparePreRelease() { 75 | JavaVersion lower = new JavaVersion("10-internal"); 76 | JavaVersion higher = new JavaVersion("10"); 77 | assertEquals(-1, lower.compareTo(higher)); 78 | } 79 | 80 | @Test 81 | public void compareMinor() { 82 | JavaVersion lower = new JavaVersion("9.0.3"); 83 | JavaVersion higher = new JavaVersion("9.0.4"); 84 | assertEquals(1, higher.compareTo(lower)); 85 | } 86 | 87 | @Test 88 | public void compareMajor() { 89 | JavaVersion lower = new JavaVersion("1.8.0_161"); 90 | JavaVersion higher = new JavaVersion("10"); 91 | assertEquals(1, higher.compareTo(lower)); 92 | } 93 | 94 | @Test 95 | public void compareEqual() { 96 | JavaVersion lower = new JavaVersion("10-Ubuntu"); 97 | JavaVersion higher = new JavaVersion("10"); 98 | assertEquals(0, lower.compareTo(higher)); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/test/java/com/github/games647/lagmonitor/util/LagUtilsTest.java: -------------------------------------------------------------------------------- 1 | package com.github.games647.lagmonitor.util; 2 | 3 | import java.util.Locale; 4 | 5 | import org.junit.jupiter.api.Test; 6 | 7 | import static org.junit.jupiter.api.Assertions.assertEquals; 8 | 9 | public class LagUtilsTest { 10 | 11 | @Test 12 | public void byteToMega() { 13 | assertEquals(1, LagUtils.byteToMega(1024 * 1024)); 14 | assertEquals(0, LagUtils.byteToMega(1000 * 1000)); 15 | } 16 | 17 | @Test 18 | public void readableBytes() { 19 | //make tests that have a constant floating point separator (, vs .) 20 | Locale.setDefault(Locale.ENGLISH); 21 | 22 | assertEquals("1.00 kiB", LagUtils.readableBytes(1024)); 23 | assertEquals("64 B", LagUtils.readableBytes(64)); 24 | assertEquals("1.00 MiB", LagUtils.readableBytes(1024 * 1024 + 12)); 25 | } 26 | } 27 | --------------------------------------------------------------------------------