Repository: games647/LagMonitor Branch: main Commit: a9a9817df967 Files: 81 Total size: 302.5 KB Directory structure: gitextract_smk98iqd/ ├── .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 ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: maven directory: "/" schedule: interval: monthly open-pull-requests-limit: 10 ignore: - dependency-name: com.github.oshi:oshi-demo versions: - "> 5.2.2, < 5.3" - dependency-name: com.github.oshi:oshi-demo versions: - "> 5.3.4, < 5.4" - dependency-name: io.netty:netty-codec versions: - "> 4.1.45.Final" - dependency-name: mysql:mysql-connector-java versions: - "> 5.1.48" - dependency-name: org.apache.maven.plugins:maven-shade-plugin versions: - "> 3.2.3, < 3.3" - dependency-name: org.apache.maven.plugins:maven-surefire-plugin versions: - "> 2.22.0, < 2.23" - dependency-name: org.mockito:mockito-junit-jupiter versions: - "> 3.4.4, < 3.5" - dependency-name: org.mockito:mockito-junit-jupiter versions: - ">= 3.5.a, < 3.6" - dependency-name: pl.project13.maven:git-commit-id-plugin versions: - "> 4.0.0, < 4.1" - dependency-name: com.github.oshi:oshi-demo versions: - 5.4.1 - 5.5.0 - 5.5.1 - 5.6.1 - 5.7.0 - dependency-name: org.mockito:mockito-junit-jupiter versions: - 3.7.7 - 3.8.0 ================================================ FILE: .gitignore ================================================ # Eclipse .classpath .project .settings/ # NetBeans nbproject/ nb-configuration.xml # IntelliJ *.iml *.ipr *.iws .idea/ # Maven target/ pom.xml.versionsBackup # Gradle .gradle # Ignore Gradle GUI config gradle-app.setting # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) !gradle-wrapper.jar # various other potential build files build/ bin/ dist/ manifest.mf *.log # Vim .*.sw[a-p] # virtual machine crash logs, see https://www.java.com/en/download/help/error_hotspot.xml hs_err_pid* # Mac filesystem dust .DS_Store ================================================ FILE: .travis.yml ================================================ # Use https://travis-ci.org/ for automatic testing # speed up testing https://blog.travis-ci.com/2014-12-17-faster-builds-with-container-based-infrastructure/ sudo: false # This is a java project language: java script: mvn test -B jdk: - oraclejdk8 - oraclejdk9 ================================================ FILE: CHANGELOG.md ================================================ # Changelog ## 1.17.1 * Make the link for monitoring clickable * Improve performance of /tasks command using MethodHandles * Fail safely on native library errors ## 1.17 * Use faster MethodHandles to lookup the player ping * Close all resources after calculating folder size * Verify if JFR methods are available in the current VM * Enable native driver by default if available * Hide vanished players from the ping command * Merge Paper and Spigot Timings parsing into one command * Improve wording for thread block or safety warnings: * If you think something is missing or could be described better, please make a pull request. * Add Thread safety checks for command events too * Ref: https://www.spigotmc.org/threads/plugins-triggering-commands-async.31815/ * Add statically compiled java version checker * Add a lot of more native Hardware and Software details: * Sensors (voltage, fan speed) * Motherboard * Networking * CPU * Java properties * Process * User * Replace outdated sigar library with oshi * Validate input for average comparison (Fixes #37) ## 1.16 * Use Bukkit's internal method to find the plugin owner * Fix checking vanilla command class check if we found an obfuscated plugin * Dynamically adjust text padding for graphs * Fix invalid threads graph name * Count the read/write of all disks * Use migration file creating MySQL table * Use MEDIUMINT for os with > 64GB of ram (Related #33) * Fix folder size calculation * Fix free ram calculation (Fixes #33) * Delay ping fetching on player join, because the first ping request is very inaccurate. ## 1.15 * Better url output for blocking http actions * Query the partition and not the filesystem for the reads/writes * Add linux distribution info * Fix total file system space ## 1.14.3 * Refactor plugin detection. Now it skips the first x entries of LagMonitor until it finds another class loader. ## 1.14.2 * Fix plugin name detection ## 1.14.1 * Fix 1.12 support ## 1.14 * Show file system type for the native command * Replace the /paper command alias with /paper-timing to prevent overrides by Paper itself ## 1.13 * Whitelist vanilla commands ## 1.12 * Filter invalid ping values * Migrate to Java 7 Path API for faster free space and other file system lookups ## 1.11.10 * Better block message descriptions ## 1.11.9 * Fix parsing hover event for 1.8 clients * Wrap to a new line only after the word * Use .spigot() for sendMessage(BaseComponent) for backwards compatibility ## 1.11.8 * Fix map listener for older minecraft version (with only one item-hand) ## 1.11.7 * Removed old debug code * Fix variable replacing in the help command ## 1.11.6 * Fixed memory leak for player pings on player quit ## 1.11.5 * Added a help page * Added new permission lagmonitor.command.help ## 1.11.4 * Fix users don't receive a map on graph command * Display error message for untracked ping players * Fail silently if the jfc file already exists ## 1.11.3 * Fix detecting socket connections (socket-block-detection) if the default proxy is null ## 1.11.2 * Optimize plugin violations handling * Fix security manager spams if enabled * Fix log caused methods only once even if it's disabled ## 1.11.1 * Add missing uri to the connection selector * Fix plugin name detection and thread-safety (Fixes #17) ## 1.11 * Added sigar as fallback when Oracle API isn't available (com.sun.management.OperatingSystemMXBean) ## 1.10.1 * Fix thread safety check ## 1.10 * Add hideStacktrace config property, which shows only two lines * Add oncePerPlugin config property which report it only one time per startup and plugin * Add a way to find the plugin source. [Experimental] ## 1.9.1 * Allow blocking actions on server startup (Fixes #15) * Clarify blocking action message * Upgrade to Java 8 (requires now Java 8) ## 1.9 * Add monitor pastes to https://paste.enginehub.org/ - Please support for this awesome service and please do not spam it * Fix showing duplicate http blocking messages, because a http connection is also a socket connection * Fix showing stacktrace on blocking action ## 1.8 * Add /lagpage < save > and /lagpage < all > ## 1.7.2 * Fix traffic reader storage save * Warn users who still use the outdated Java 7 to upgrade to a newer version ## 1.7 * Fail safely on an error for traffic reader * Add configurable table prefix * Add debug code if the storage insert failed ## 1.6 * Added whitelist for certain commands for specific users ## 1.5 * Added a faster and less error-prone blocking http detection ## 1.4 * Added monitoring to a MySQL database * Added a unsupported java vendor hint to heap and thread dumps * Speed up the native command by loading the native driver only on plugin load ## 1.3.2 * Fix command permission for /ping player ## 1.3.1 * Fix class not found in paper spigot timings parser if user is using normal spigot ## 1.3 * Added PaperTimings head data * Added percent values to the paper spigot timings * Fixed combined plugin name * Fixed unknown entries in paper spigot timings parser * Fixed missing total second head data in spigot timings parser * Fixed pagination error from the last page ## 1.2 * Added support for Java Flight Recorder dump * Added default configuration file for flight recorder * Fixed permission of lagpage command has the paper command permission ## 1.1 * Added thread dump to file option /thread dump * Added heap dump to file option /heap dump * Fix pagination error if the user is requesting a too high page number ## 1.0 * Added plugin injection (commands, listener and tasks) * Added pagination * Added /heap command for heap dumps * Added world size to the system command * Added tile entities count to the system command * Added security manager for more efficient blocking checks * Added combined graphs example: /graph cpu heap threads * Added check if timings is enabled for Paper servers * Improved performance of commands by caching them with the pagination * Optimize Spigot timings parser ## 0.7 * Added /lag alias for the /tpshistory command * Added swap to the environment command * Added tasks command * Added /vm command for class loading, garbage collectors, vm specifications * Added basic Paper timings parser * Added load average to the environment command * Moved Java version to the vm command * Optimized thread locking in monitor/profiler for better performance ## 0.6 * Added /native command to query native data like OS uptime, Network adapter speed, CPU MHZ, ... * Added startup parameters to the system command * Added thread-safety check * Added blocking, waiting, sleeping check * Added Thread id to the threads command * Improved readability for tpshistory command in console * Fixed very low tps value displayed as full tps * Fixed scrolling tpsHistory * Fixed NPE on plugin load at runtime * Fixes ClassNotFoundException on reload if traffic reader is activated * Fixed rounding issues for the average ping * Fixed cleanup of monitor task on plugin disable ## 0.5 * Added Ping History -> displays average ping now * Added traffic counter * Added config * Reduce memory usage by getting the stacktrace of only one thread * Fixed thread safety ## 0.4 * Added world info to the system command * Added lazy loading for thread monitor to reduce memory usage * Added worlds, players and plugins count to the system command * Added samples count for thread monitor * Improved tons of command styling * Fixed thread safety * Fixed free memory value * Fixed memory leak for thread monitor * Fixed ping method only displaying the own ping ## 0.3 * Fixed: max memory output in the /system command * Added color highlighting for performance intensive tasks in the timings report * Fixed timings output * Added warning if timings are deactivated * Added classes graph * Added command completion for all commands * Updated to Minecraft 1.9 * Added missing permission node for /ping [player] to the plugin.yml ## 0.2 * Added environment command * Added server version to the system command * Added more graphs (Threads, CPU usage) * Fixed CPU usage value * Improved Command output styling * Reduced delay start of ticks per second task ## 0.1.1 * Added command permissions * Added online check for the ping command ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2016-2018 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # LagMonitor ## Description Gives you the possibility to monitor your server performance. This plugin is based on the powerful tools VisualVM and Java Mission Control, both provided by Oracle. This plugin gives you the possibility to use the features provided by these tools also in Minecraft itself. This might be useful for server owners/administrators who cannot use the tools. Furthermore, it is especially made for Minecraft itself. So you can also check your TPS (Ticks per second), player ping, server timings and so on. ## Features * Player ping * Offline Java version checker * Thread safety checks * Many details about your setup like Hardware (Disk, Processor, ...) and about your OS * Sample CPU usage * Analyze RAM usage * Access to Stacktraces of running threads * Shows your ticks per second with history * Shows system performance usage * Visual graphs in-game * In-game timings viewer * Access to Java environment variables (mbeans) * Plugin specific profiles * Blocking operations on the main thread check * Make Heap and Thread dumps * Create Java Flight Recorder dump and analyze it later on your own computer * Log the server performance into a MySQL/MariaDB database ## Requirements * Java 8+ * Spigot 1.8.8+ or a fork of it (ex: Paper) ## Permissions lagmonitor.* - Access to all LagMonitor features lagmonitor.commands.* - Access to all commands ### All command permissions * lagmonitor.command.ping * lagmonitor.command.ping.other * lagmonitor.command.stacktrace * lagmonitor.command.thread * lagmonitor.command.tps * lagmonitor.command.mbean * lagmonitor.command.system * lagmonitor.command.environment * lagmonitor.command.timing * lagmonitor.command.monitor * lagmonitor.command.graph * lagmonitor.command.native * lagmonitor.command.vm * lagmonitor.command.network * lagmonitor.command.tasks * lagmonitor.command.heap * lagmonitor.command.jfr ## Commands /ping - Gets your server ping /ping - Gets the ping of the selected player /stacktrace - Gets the execution stacktrace of the current thread /stacktrace - Gets the execution stacktrace of selected thread /thread - Outputs all running threads with their current state /tpshistory - Outputs the current tps /mbean - List all available mbeans (java environment information, JMX) /mbean - List all available attributes of this mbean /mbean - Outputs the value of this attribute /system - Gives you some general information (Minecraft server related) /env - Gives you some general information (OS related) /timing - Outputs your server timings ingame /monitor [start|stop|paste] - Monitors the CPU usage of methods /graph [heap|cpu|threads|classes] - Gives you visual graph about your server (currently only the heap usage) /native - Gives you some native os information /vm - Outputs vm specific information like garbage collector, class loading or vm specification /network - Shows network interface configuration /tasks - Information about running and pending tasks /heap - Heap dump about your current memory /lagpage - Pagination command for the current pagination session /jfr - Manages the Java Flight Recordings of the native Java VM. It gives you much more detailed information including network communications, file read/write times, detailed heap and thread data, ... ## Development builds Development builds of this project can be acquired at the provided CI (continuous integration) server. It contains the latest changes from the Source-Code in preparation for the following release. This means they could contain new features, bug fixes and other changes since the last release. Nevertheless builds are only tested using a small set of automated and a few manual tests. Therefore they **could** contain new bugs and are likely to be less stable than released versions. https://ci.codemc.org/job/Games647/job/LagMonitor/changes ## Network requests This plugin performs network requests to: * https://paste.enginehub.org - uploading monitor paste command outputs ## Reproducible builds This project supports reproducible builds for enhanced security. In short, this means that the source code matches the generated built jar file. Outputs could vary by operating system (line endings), different JDK versions and build timestamp. You can extract this using [build-info](https://github.com/apache/maven-studies/tree/maven-buildinfo-plugin). Once you have the configuration to use the same line endings and JDK version, you can use the following command to inject a custom build timestamp to complete the configuration. `mvn clean install -Dproject.build.outputTimestamp=DATE` ## Images ### Heap command ![heap command](https://i.imgur.com/AzDwYxq.png) ### Timing command ![timing command](https://i.imgur.com/wAxnIxt.png) ### CPU Graph (blue=process, yellow=system) - Process load ![cpu graph](https://i.imgur.com/DajnZmP.png) ### Stacktrace and Threads command ![stacktrace and threads](https://i.imgur.com/XY7r9wz.png) ### Ping Command ![ping command](https://i.imgur.com/LITJKWw.png) ### Thread Sampler (Monitor command) ![thread sample](https://i.imgur.com/OXOakN6.png) ### System command ![system command](https://i.imgur.com/hrIV6bW.png) ### Environment command ![environment command](https://i.imgur.com/gQwr126.png) ### Heap usage graph (yellow=allocated, blue=used) ![heap usage map](https://i.imgur.com/Yiz9h6G.png) ================================================ FILE: pom.xml ================================================ 4.0.0 com.github.games647 lagmonitor jar LagMonitor 1.17.3 https://dev.bukkit.org/bukkit-plugins/LagMonitor/ Monitors your Minecraft server for Lags UTF-8 10 10 10 5.9.2 6.4.1 1.19.4-R0.1-SNAPSHOT install ${project.name} org.apache.maven.plugins maven-jar-plugin 3.2.0 org.apache.maven.plugins maven-shade-plugin 3.2.4 false oshi lagmonitor.oshi *.properties oshi.architecture.properties oshi.linux.filename.properties oshi.macos.versions.properties oshi.properties oshi.vmmacaddr.properties net.java.dev.jna:jna package shade pl.project13.maven git-commit-id-plugin 4.9.10 false get-the-git-infos revision org.apache.maven.plugins maven-surefire-plugin 2.22.2 src/main/resources true spigot-repo https://hub.spigotmc.org/nexus/content/repositories/snapshots/ paper-repo https://repo.papermc.io/repository/maven-snapshots/ io.papermc.paper paper-api ${spigotApi} provided net.md-5 bungeecord-chat org.spigotmc spigot-api ${spigotApi} provided com.github.oshi oshi-demo ${oshi.version} com.fasterxml.jackson.core jackson-databind org.jfree jfreechart org.slf4j slf4j-jdk14 2.0.7 com.github.oshi oshi-core ${oshi.version} mysql mysql-connector-java 8.0.32 provided io.netty netty-codec 4.1.45.Final provided org.junit.jupiter junit-jupiter-api ${junit.jupiter.version} test org.junit.jupiter junit-jupiter-engine ${junit.jupiter.version} test org.mockito mockito-junit-jupiter 5.2.0 test org.hamcrest hamcrest-core 2.2 test ================================================ FILE: src/main/java/com/github/games647/lagmonitor/LagMonitor.java ================================================ package com.github.games647.lagmonitor; import com.github.games647.lagmonitor.command.EnvironmentCommand; import com.github.games647.lagmonitor.command.GraphCommand; import com.github.games647.lagmonitor.command.HelpCommand; import com.github.games647.lagmonitor.command.MbeanCommand; import com.github.games647.lagmonitor.command.MonitorCommand; import com.github.games647.lagmonitor.command.NativeCommand; import com.github.games647.lagmonitor.command.NetworkCommand; import com.github.games647.lagmonitor.command.PaginationCommand; import com.github.games647.lagmonitor.command.StackTraceCommand; import com.github.games647.lagmonitor.command.VmCommand; import com.github.games647.lagmonitor.command.dump.FlightCommand; import com.github.games647.lagmonitor.command.dump.HeapCommand; import com.github.games647.lagmonitor.command.dump.ThreadCommand; import com.github.games647.lagmonitor.command.minecraft.PingCommand; import com.github.games647.lagmonitor.command.minecraft.SystemCommand; import com.github.games647.lagmonitor.command.minecraft.TPSCommand; import com.github.games647.lagmonitor.command.minecraft.TasksCommand; import com.github.games647.lagmonitor.command.timing.PaperTimingsCommand; import com.github.games647.lagmonitor.command.timing.SpigotTimingsCommand; import com.github.games647.lagmonitor.listener.BlockingConnectionSelector; import com.github.games647.lagmonitor.listener.GraphListener; import com.github.games647.lagmonitor.listener.PageManager; import com.github.games647.lagmonitor.listener.ThreadSafetyListener; import com.github.games647.lagmonitor.logging.ForwardingLoggerFactory; import com.github.games647.lagmonitor.storage.MonitorSaveTask; import com.github.games647.lagmonitor.storage.NativeSaveTask; import com.github.games647.lagmonitor.storage.Storage; import com.github.games647.lagmonitor.storage.TPSSaveTask; import com.github.games647.lagmonitor.task.IODetectorTask; import com.github.games647.lagmonitor.task.PingManager; import com.github.games647.lagmonitor.task.TPSHistoryTask; import com.github.games647.lagmonitor.threading.BlockingActionManager; import com.github.games647.lagmonitor.threading.BlockingSecurityManager; import com.github.games647.lagmonitor.threading.Injectable; import com.github.games647.lagmonitor.traffic.TrafficReader; import java.net.ProxySelector; import java.nio.file.Files; import java.sql.SQLException; import java.time.Duration; import java.util.Optional; import java.util.Timer; import java.util.logging.Level; import org.bukkit.Bukkit; import org.bukkit.command.PluginCommand; import org.bukkit.plugin.PluginManager; import org.bukkit.plugin.java.JavaPlugin; import org.bukkit.scheduler.BukkitScheduler; public class LagMonitor extends JavaPlugin { private static final int DETECTION_THRESHOLD = 10; private static final int HOURS_PER_DAY = 24; private static final int MINUTES_PER_HOUR = 60; private static final int SECONDS_PER_MINUTE = 60; private final BlockingActionManager actionManager = new BlockingActionManager(this); private final PageManager pageManager = new PageManager(); private final TPSHistoryTask tpsHistoryTask = new TPSHistoryTask(); private final NativeManager nativeData = new NativeManager(getLogger(), getDataFolder().toPath()); private PingManager pingManager; private TrafficReader trafficReader; private Timer blockDetectionTimer; private Timer monitorTimer; @Override public void onLoad() { ForwardingLoggerFactory.PARENT_LOGGER = getLogger(); nativeData.setupNativeAdapter(); } @Override public void onEnable() { saveDefaultConfig(); if (Files.notExists(getDataFolder().toPath().resolve("default.jfc"))) { saveResource("default.jfc", false); } try { pingManager = new PingManager(this); } catch (ReflectiveOperationException reflectiveEx) { getLogger().log(Level.SEVERE, "Cannot initialize ping manager", reflectiveEx); } //register schedule tasks BukkitScheduler scheduler = getServer().getScheduler(); scheduler.runTaskTimer(this, tpsHistoryTask, 20L, TPSHistoryTask.RUN_INTERVAL); scheduler.runTaskTimer(this, pingManager, 20L, PingManager.PING_INTERVAL); //register listeners PluginManager pluginManager = getServer().getPluginManager(); pluginManager.registerEvents(new GraphListener(), this); pluginManager.registerEvents(pageManager, this); pluginManager.registerEvents(pingManager, this); //add the player to the list in the case the plugin is loaded at runtime Bukkit.getOnlinePlayers().forEach(pingManager::addPlayer); if (getConfig().getBoolean("traffic-counter")) { try { trafficReader = new TrafficReader(this); } catch (Exception ex) { getLogger().log(Level.SEVERE, "Failed to initialize packet reader", ex); } } if (getConfig().getBoolean("thread-safety-check")) { pluginManager.registerEvents(new ThreadSafetyListener(actionManager), this); } if (getConfig().getBoolean("thread-block-detection")) { scheduler.runTask(this, () -> { blockDetectionTimer = new Timer(getName() + "-Thread-Blocking-Detection"); IODetectorTask detectorTask = new IODetectorTask(actionManager, Thread.currentThread()); blockDetectionTimer.scheduleAtFixedRate(detectorTask, DETECTION_THRESHOLD, DETECTION_THRESHOLD); }); } if (getConfig().getBoolean("monitor-database")) { setupMonitoringDatabase(); } if (getConfig().getBoolean("socket-block-detection")) { scheduler.runTask(this, () -> new BlockingConnectionSelector(actionManager).inject()); } if (getConfig().getBoolean("securityMangerBlockingCheck")) { if (Runtime.version().feature() < 17) { scheduler.runTask(this, () -> new BlockingSecurityManager(actionManager).inject()); } } registerCommands(); } private void setupMonitoringDatabase() { try { String host = getConfig().getString("host"); int port = getConfig().getInt("port"); String database = getConfig().getString("database"); boolean useSSL = getConfig().getBoolean("useSSL"); String username = getConfig().getString("username"); String password = getConfig().getString("password"); String tablePrefix = getConfig().getString("tablePrefix"); Storage storage = new Storage(getLogger(), host, port, database, useSSL, username, password, tablePrefix); storage.createTables(); BukkitScheduler scheduler = getServer().getScheduler(); scheduler.runTaskTimerAsynchronously(this, new TPSSaveTask(tpsHistoryTask, storage), 20L, getConfig().getInt("tps-save-interval") * 20L); //this can run async because it runs independently of the main thread scheduler.runTaskTimerAsynchronously(this, new MonitorSaveTask(this, storage), 20L,getConfig().getInt("monitor-save-interval") * 20L); scheduler.runTaskTimerAsynchronously(this, new NativeSaveTask(this, storage), 20L,getConfig().getInt("native-save-interval") * 20L); } catch (SQLException sqlEx) { getLogger().log(Level.SEVERE, "Failed to setup monitoring database", sqlEx); } } @Override public void onDisable() { if (trafficReader != null) { trafficReader.close(); trafficReader = null; } close(blockDetectionTimer); blockDetectionTimer = null; close(monitorTimer); monitorTimer = null; //restore the security manager SecurityManager securityManager = System.getSecurityManager(); if (securityManager instanceof BlockingSecurityManager) { ((Injectable) securityManager).restore(); } ProxySelector proxySelector = ProxySelector.getDefault(); if (proxySelector instanceof BlockingConnectionSelector) { ((Injectable) proxySelector).restore(); } } private void close(Timer timer) { if (timer != null) { timer.cancel(); timer.purge(); } } public PageManager getPageManager() { return pageManager; } public Timer getMonitorTimer() { return monitorTimer; } public void setMonitorTimer(Timer monitorTimer) { this.monitorTimer = monitorTimer; } public TrafficReader getTrafficReader() { return trafficReader; } public TPSHistoryTask getTpsHistoryTask() { return tpsHistoryTask; } public Optional getPingManager() { return Optional.ofNullable(pingManager); } public NativeManager getNativeData() { return nativeData; } private void registerCommands() { getCommand(getName()).setExecutor(new HelpCommand(this)); getCommand("ping").setExecutor(new PingCommand(this)); getCommand("stacktrace").setExecutor(new StackTraceCommand(this)); getCommand("thread").setExecutor(new ThreadCommand(this)); getCommand("tpshistory").setExecutor(new TPSCommand(this)); getCommand("mbean").setExecutor(new MbeanCommand(this)); getCommand("system").setExecutor(new SystemCommand(this)); getCommand("env").setExecutor(new EnvironmentCommand(this)); getCommand("monitor").setExecutor(new MonitorCommand(this)); getCommand("graph").setExecutor(new GraphCommand(this)); getCommand("native").setExecutor(new NativeCommand(this)); getCommand("vm").setExecutor(new VmCommand(this)); getCommand("tasks").setExecutor(new TasksCommand(this)); getCommand("heap").setExecutor(new HeapCommand(this)); getCommand("lagpage").setExecutor(new PaginationCommand(this)); getCommand("jfr").setExecutor(new FlightCommand(this)); getCommand("network").setExecutor(new NetworkCommand(this)); PluginCommand timing = getCommand("timing"); try { //paper moved to class to package co.aikar.timings Class.forName("co.aikar.timings.Timing"); timing.setExecutor(new PaperTimingsCommand(this)); } catch (ClassNotFoundException e) { timing.setExecutor(new SpigotTimingsCommand(this)); } } public static String formatDuration(Duration duration) { long seconds = duration.getSeconds(); return String.format("'%d' days '%d' hours '%d' minutes '%d' seconds'", duration.toDays(), duration.toHours() % HOURS_PER_DAY, duration.toMinutes() % MINUTES_PER_HOUR, duration.getSeconds() % SECONDS_PER_MINUTE); } } ================================================ FILE: src/main/java/com/github/games647/lagmonitor/MethodMeasurement.java ================================================ package com.github.games647.lagmonitor; import com.google.common.collect.ImmutableMap; import java.util.HashMap; import java.util.Map; import java.util.Objects; import java.util.stream.IntStream; public class MethodMeasurement implements Comparable { private final String id; private final String className; private final String method; private final Map childInvokes = new HashMap<>(); private long totalTime; public MethodMeasurement(String id, String className, String method) { this.id = id; this.className = className; this.method = method; } public String getId() { return id; } public String getClassName() { return className; } public String getMethod() { return method; } public long getTotalTime() { return totalTime; } public Map getChildInvokes() { return ImmutableMap.copyOf(childInvokes); } public float getTimePercent(long parentTime) { //one float conversion triggers the complete calculation to be decimal return ((float) totalTime / parentTime) * 100; } public void onMeasurement(StackTraceElement[] stackTrace, int skipElements, long time) { totalTime += time; if (skipElements >= stackTrace.length) { //we reached the end return; } StackTraceElement nextChildElement = stackTrace[stackTrace.length - skipElements - 1]; String nextClass = nextChildElement.getClassName(); String nextMethod = nextChildElement.getMethodName(); String idName = nextChildElement.getClassName() + '.' + nextChildElement.getMethodName(); MethodMeasurement child = childInvokes .computeIfAbsent(idName, (key) -> new MethodMeasurement(key, nextClass, nextMethod)); child.onMeasurement(stackTrace, skipElements + 1, time); } public void writeString(StringBuilder builder, int indent) { StringBuilder b = new StringBuilder(); IntStream.range(0, indent).forEach(i -> b.append(' ')); String padding = b.toString(); for (MethodMeasurement child : getChildInvokes().values()) { builder.append(padding).append(child.id).append("()"); builder.append(' '); builder.append(child.totalTime).append("ms"); builder.append('\n'); child.writeString(builder, indent + 1); } } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; MethodMeasurement that = (MethodMeasurement) o; return totalTime == that.totalTime && Objects.equals(id, that.id) && Objects.equals(className, that.className) && Objects.equals(method, that.method) && Objects.equals(childInvokes, that.childInvokes); } @Override public int hashCode() { return Objects.hash(id, className, method, childInvokes, totalTime); } @Override public int compareTo(MethodMeasurement other) { return Long.compare(this.totalTime, other.totalTime); } @Override public String toString() { StringBuilder builder = new StringBuilder(); for (Map.Entry entry : getChildInvokes().entrySet()) { builder.append(entry.getKey()).append("()"); builder.append(' '); builder.append(entry.getValue().totalTime).append("ms"); builder.append('\n'); entry.getValue().writeString(builder, 1); } return builder.toString(); } } ================================================ FILE: src/main/java/com/github/games647/lagmonitor/NativeManager.java ================================================ package com.github.games647.lagmonitor; import com.sun.management.UnixOperatingSystemMXBean; import java.io.IOException; import java.lang.management.ManagementFactory; import java.lang.management.OperatingSystemMXBean; import java.nio.file.FileStore; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Optional; import java.util.logging.Level; import java.util.logging.Logger; import oshi.SystemInfo; import oshi.hardware.GlobalMemory; import oshi.software.os.OSProcess; public class NativeManager { private static final String JNA_FILE = "jna-5.5.0.jar"; private final Logger logger; private final Path dataFolder; private final OperatingSystemMXBean osBean = ManagementFactory.getOperatingSystemMXBean(); private SystemInfo info; private int pid = -1; public NativeManager(Logger logger, Path dataFolder) { this.logger = logger; this.dataFolder = dataFolder; } public void setupNativeAdapter() { logger.info("Found JNA native library. Enabling extended native data support to display more data"); try { info = new SystemInfo(); //make a test call pid = info.getOperatingSystem().getProcessId(); } catch (UnsatisfiedLinkError | NoClassDefFoundError linkError) { logger.log(Level.INFO, "Cannot load native library. Continuing without it...", linkError); info = null; } } public Optional getSystemInfo() { return Optional.ofNullable(info); } public double getProcessCPULoad() { if (osBean instanceof com.sun.management.OperatingSystemMXBean) { com.sun.management.OperatingSystemMXBean nativeOsBean = (com.sun.management.OperatingSystemMXBean) osBean; return nativeOsBean.getProcessCpuLoad(); } return -1; } public Optional getProcess() { if (info == null) { return Optional.empty(); } return Optional.of(info.getOperatingSystem().getProcess(pid)); } public double getCPULoad() { if (osBean instanceof com.sun.management.OperatingSystemMXBean) { com.sun.management.OperatingSystemMXBean nativeOsBean = (com.sun.management.OperatingSystemMXBean) osBean; return nativeOsBean.getSystemCpuLoad(); } else if (info != null) { return info.getHardware().getProcessor().getSystemLoadAverage(1)[0]; } return -1; } public long getOpenFileDescriptors() { if (osBean instanceof com.sun.management.UnixOperatingSystemMXBean) { return ((UnixOperatingSystemMXBean) osBean).getOpenFileDescriptorCount(); } else if (info != null) { return info.getOperatingSystem().getFileSystem().getOpenFileDescriptors(); } return -1; } public long getMaxFileDescriptors() { if (osBean instanceof com.sun.management.UnixOperatingSystemMXBean) { return ((UnixOperatingSystemMXBean) osBean).getMaxFileDescriptorCount(); } else if (info != null) { return info.getOperatingSystem().getFileSystem().getMaxFileDescriptors(); } return -1; } public long getTotalMemory() { if (osBean instanceof com.sun.management.OperatingSystemMXBean) { com.sun.management.OperatingSystemMXBean nativeOsBean = (com.sun.management.OperatingSystemMXBean) osBean; return nativeOsBean.getTotalPhysicalMemorySize(); } else if (info != null) { return info.getHardware().getMemory().getTotal(); } return -1; } public long getFreeMemory() { if (osBean instanceof com.sun.management.OperatingSystemMXBean) { com.sun.management.OperatingSystemMXBean nativeOsBean = (com.sun.management.OperatingSystemMXBean) osBean; return nativeOsBean.getFreePhysicalMemorySize(); } else if (info != null) { return getTotalMemory() - info.getHardware().getMemory().getAvailable(); } return -1; } public long getFreeSwap() { if (osBean instanceof com.sun.management.OperatingSystemMXBean) { com.sun.management.OperatingSystemMXBean nativeOsBean = (com.sun.management.OperatingSystemMXBean) osBean; return nativeOsBean.getFreeSwapSpaceSize(); } else if (info != null) { GlobalMemory memory = info.getHardware().getMemory(); return memory.getAvailable(); } return -1; } public long getTotalSwap() { if (osBean instanceof com.sun.management.OperatingSystemMXBean) { com.sun.management.OperatingSystemMXBean nativeOsBean = (com.sun.management.OperatingSystemMXBean) osBean; return nativeOsBean.getTotalSwapSpaceSize(); } else if (info != null) { return info.getHardware().getMemory().getVirtualMemory().getSwapTotal(); } return -1; } public long getFreeSpace() { long freeSpace = 0; try { FileStore fileStore = Files.getFileStore(Paths.get(".")); freeSpace = fileStore.getUsableSpace(); } catch (IOException ioEx) { logger.log(Level.WARNING, "Cannot calculate free/total disk space", ioEx); } return freeSpace; } public long getTotalSpace() { long totalSpace = 0; try { FileStore fileStore = Files.getFileStore(Paths.get(".")); totalSpace = fileStore.getTotalSpace(); } catch (IOException ioEx) { logger.log(Level.WARNING, "Cannot calculate free disk space", ioEx); } return totalSpace; } } ================================================ FILE: src/main/java/com/github/games647/lagmonitor/Pages.java ================================================ package com.github.games647.lagmonitor; import java.time.LocalTime; import java.time.format.DateTimeFormatter; import java.time.format.FormatStyle; import java.util.List; import net.md_5.bungee.api.ChatColor; import net.md_5.bungee.api.chat.BaseComponent; import net.md_5.bungee.api.chat.ClickEvent; import net.md_5.bungee.api.chat.ComponentBuilder; import net.md_5.bungee.api.chat.ComponentBuilder.FormatRetention; import net.md_5.bungee.api.chat.HoverEvent; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; import org.bukkit.util.ChatPaginator; public class Pages { private static final int PAGINATION_LINES = 2; private static final int CONSOLE_HEIGHT = 40 - PAGINATION_LINES; private static final int PLAYER_HEIGHT = ChatPaginator.OPEN_CHAT_PAGE_HEIGHT - PAGINATION_LINES; public static String filterPackageNames(String packageName) { String text = packageName; if (text.contains("net.minecraft.server")) { text = text.replace("net.minecraft.server", "NMS"); } else if (text.contains("org.bukkit.craftbukkit")) { text = text.replace("org.bukkit.craftbukkit", "OBC"); } //IDEA: if it's a player we need to shorten the text more aggressively //maybe replacing the package with the plugin name //by getting the package name from the plugin.yml? return text; } private final String date = LocalTime.now().format(DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT)); private final String title; private final List lines; private int lastSentPage = 1; public Pages(String title, List lines) { this.title = title; this.lines = lines; } public int getTotalPages(boolean isPlayer) { if (isPlayer) { return (lines.size() / PLAYER_HEIGHT) + 1; } return (lines.size() / CONSOLE_HEIGHT) + 1; } public List getAllLines() { return lines; } public int getLastSentPage() { return lastSentPage; } public void setLastSentPage(int lastSentPage) { this.lastSentPage = lastSentPage; } public List getPage(int page, boolean isPlayer) { int startIndex; int endIndex; if (isPlayer) { startIndex = (page - 1) * PLAYER_HEIGHT; endIndex = page * PLAYER_HEIGHT; } else { startIndex = (page - 1) * CONSOLE_HEIGHT; endIndex = page * CONSOLE_HEIGHT; } if (startIndex >= lines.size()) { endIndex = lines.size() - 1; startIndex = endIndex; } else if (endIndex >= lines.size()) { endIndex = lines.size() - 1; } return lines.subList(startIndex, endIndex); } public BaseComponent[] buildHeader(int page, int totalPages) { return new ComponentBuilder(title + " from " + date) .color(ChatColor.GOLD) .append(" << ") .event(new HoverEvent(HoverEvent.Action.SHOW_TEXT , new ComponentBuilder("Go to the previous page").create())) .event(new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/lagpage " + (page - 1))) .color(ChatColor.DARK_AQUA) .append(page + " / " + totalPages, FormatRetention.NONE) .color(ChatColor.GRAY) .append(" >>") .event(new HoverEvent(HoverEvent.Action.SHOW_TEXT , new ComponentBuilder("Go to the next page").create())) .event(new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/lagpage " + (page + 1))) .color(ChatColor.DARK_AQUA) .create(); } public String buildFooter(int page, boolean isPlayer) { int endIndex; if (isPlayer) { endIndex = page * PLAYER_HEIGHT; } else { endIndex = page * CONSOLE_HEIGHT; } if (endIndex < lines.size()) { //Index starts by 0 int remaining = lines.size() - endIndex - 1; return "... " + remaining + " more entries. Click the arrows above or type /lagpage next"; } return ""; } public void send(CommandSender sender) { send(sender, 1); } public void send(CommandSender sender, int page) { this.lastSentPage = page; if (sender instanceof Player) { Player player = (Player) sender; player.spigot().sendMessage(buildHeader(page, getTotalPages(true))); getPage(page, true).forEach(player.spigot()::sendMessage); String footer = buildFooter(page, true); if (!footer.isEmpty()) { sender.sendMessage(ChatColor.GOLD + footer); } } else { BaseComponent[] header = buildHeader(page, getTotalPages(false)); StringBuilder headerBuilder = new StringBuilder(); for (BaseComponent component : header) { headerBuilder.append(component.toLegacyText()); } sender.sendMessage(headerBuilder.toString()); getPage(page, false).stream().map(line -> { StringBuilder lineBuilder = new StringBuilder(); for (BaseComponent component : line) { lineBuilder.append(component.toLegacyText()); } return lineBuilder.toString(); }).forEach(sender::sendMessage); String footer = buildFooter(page, false); if (!footer.isEmpty()) { sender.sendMessage(ChatColor.GOLD + footer); } } } } ================================================ FILE: src/main/java/com/github/games647/lagmonitor/command/EnvironmentCommand.java ================================================ package com.github.games647.lagmonitor.command; import com.github.games647.lagmonitor.LagMonitor; import com.github.games647.lagmonitor.NativeManager; import java.lang.management.ManagementFactory; import java.lang.management.OperatingSystemMXBean; import java.text.DecimalFormat; import java.util.Map.Entry; import java.util.Optional; import oshi.SystemInfo; import oshi.hardware.CentralProcessor; import oshi.hardware.CentralProcessor.ProcessorIdentifier; import oshi.software.os.OperatingSystem; import org.bukkit.command.Command; import org.bukkit.command.CommandSender; import static com.github.games647.lagmonitor.util.LagUtils.readableBytes; public class EnvironmentCommand extends LagCommand { public EnvironmentCommand(LagMonitor plugin) { super(plugin); } @Override public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { if (!canExecute(sender, command)) { return true; } OperatingSystemMXBean osBean = ManagementFactory.getOperatingSystemMXBean(); //os general info sendMessage(sender, "OS Name", osBean.getName()); sendMessage(sender, "OS Arch", osBean.getArch()); Optional optInfo = plugin.getNativeData().getSystemInfo(); if (optInfo.isPresent()) { SystemInfo systemInfo = optInfo.get(); OperatingSystem osInfo = systemInfo.getOperatingSystem(); sendMessage(sender, "OS family", osInfo.getFamily()); sendMessage(sender, "OS version", osInfo.getVersionInfo().toString()); sendMessage(sender, "OS Manufacturer", osInfo.getManufacturer()); sendMessage(sender, "Total processes", String.valueOf(osInfo.getProcessCount())); sendMessage(sender, "Total threads", String.valueOf(osInfo.getThreadCount())); } //CPU sender.sendMessage(PRIMARY_COLOR + "CPU:"); if (optInfo.isPresent()) { CentralProcessor processor = optInfo.get().getHardware().getProcessor(); ProcessorIdentifier identifier = processor.getProcessorIdentifier(); sendMessage(sender, " Vendor", identifier.getVendor()); sendMessage(sender, " Family", identifier.getFamily()); sendMessage(sender, " Name", identifier.getName()); sendMessage(sender, " Model", identifier.getModel()); sendMessage(sender, " Id", identifier.getIdentifier()); sendMessage(sender, " Vendor freq", String.valueOf(identifier.getVendorFreq())); sendMessage(sender, " Physical Cores", String.valueOf(processor.getPhysicalProcessorCount())); } sendMessage(sender, " Logical Cores", String.valueOf(osBean.getAvailableProcessors())); sendMessage(sender, " Endian", System.getProperty("sun.cpu.endian", "Unknown")); sendMessage(sender, "Load Average", String.valueOf(osBean.getSystemLoadAverage())); printExtendOsInfo(sender); displayDiskSpace(sender); NativeManager nativeData = plugin.getNativeData(); sendMessage(sender, "Open file descriptors", String.valueOf(nativeData.getOpenFileDescriptors())); sendMessage(sender, "Max file descriptors", String.valueOf(nativeData.getMaxFileDescriptors())); sender.sendMessage(PRIMARY_COLOR + "Variables:"); for (Entry variable : System.getenv().entrySet()) { sendMessage(sender, " " + variable.getKey(), variable.getValue()); } return true; } private void printExtendOsInfo(CommandSender sender) { NativeManager nativeData = plugin.getNativeData(); //cpu double systemCpuLoad = nativeData.getCPULoad(); double processCpuLoad = nativeData.getProcessCPULoad(); //these numbers are in percent (0.01 -> 1%) //we want to to have four places in a human readable percent value to multiple it with 100 DecimalFormat decimalFormat = new DecimalFormat("###.#### %"); decimalFormat.setMultiplier(100); String systemLoadFormat = decimalFormat.format(systemCpuLoad); String processLoadFormat = decimalFormat.format(processCpuLoad); sendMessage(sender,"System Usage", systemLoadFormat); sendMessage(sender,"Process Usage", processLoadFormat); //swap long totalSwap = nativeData.getTotalSwap(); long freeSwap = nativeData.getFreeSwap(); sendMessage(sender, "Total Swap", readableBytes(totalSwap)); sendMessage(sender, "Free Swap", readableBytes(freeSwap)); //RAM long totalMemory = nativeData.getTotalMemory(); long freeMemory = nativeData.getFreeMemory(); sendMessage(sender, "Total OS RAM", readableBytes(totalMemory)); sendMessage(sender, "Free OS RAM", readableBytes(freeMemory)); } private void displayDiskSpace(CommandSender sender) { long freeSpace = plugin.getNativeData().getFreeSpace(); long totalSpace = plugin.getNativeData().getTotalSpace(); //Disk info sendMessage(sender,"Disk Size", readableBytes(totalSpace)); sendMessage(sender,"Free Space", readableBytes(freeSpace)); } } ================================================ FILE: src/main/java/com/github/games647/lagmonitor/command/GraphCommand.java ================================================ package com.github.games647.lagmonitor.command; import com.github.games647.lagmonitor.LagMonitor; import com.github.games647.lagmonitor.graph.ClassesGraph; import com.github.games647.lagmonitor.graph.CombinedGraph; import com.github.games647.lagmonitor.graph.CpuGraph; import com.github.games647.lagmonitor.graph.GraphRenderer; import com.github.games647.lagmonitor.graph.HeapGraph; import com.github.games647.lagmonitor.graph.ThreadsGraph; import com.github.games647.lagmonitor.util.LagUtils; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import org.bukkit.Bukkit; import org.bukkit.ChatColor; import org.bukkit.Material; import org.bukkit.command.Command; import org.bukkit.command.CommandSender; import org.bukkit.command.TabExecutor; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.PlayerInventory; import org.bukkit.inventory.meta.ItemMeta; import org.bukkit.inventory.meta.MapMeta; import org.bukkit.map.MapView; import static java.util.stream.Collectors.toList; public class GraphCommand extends LagCommand implements TabExecutor { private static final int MAX_COMBINED = 4; private final Map graphTypes = new HashMap<>(); public GraphCommand(LagMonitor plugin) { super(plugin); graphTypes.put("classes", new ClassesGraph()); graphTypes.put("cpu", new CpuGraph(plugin, plugin.getNativeData())); graphTypes.put("heap", new HeapGraph()); graphTypes.put("threads", new ThreadsGraph()); } @Override public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { if (!canExecute(sender, command)) { return true; } if (sender instanceof Player) { Player player = (Player) sender; if (args.length > 0) { if (args.length > 1) { buildCombinedGraph(player, args); } else { String graph = args[0]; GraphRenderer renderer = graphTypes.get(graph); if (renderer == null) { sendError(sender, "Unknown graph type"); } else { giveMap(player, installRenderer(player, renderer)); } } return true; } //default is heap usage GraphRenderer graphRenderer = graphTypes.get("heap"); MapView mapView = installRenderer(player, graphRenderer); giveMap(player, mapView); } else { sendError(sender, "Not implemented for the console"); } return true; } @Override public List onTabComplete(CommandSender sender, Command command, String alias, String[] args) { if (args.length != 1) { return Collections.emptyList(); } String lastArg = args[args.length - 1]; return graphTypes.keySet().stream() .filter(type -> type.startsWith(lastArg)) .sorted(String.CASE_INSENSITIVE_ORDER) .collect(toList()); } private void buildCombinedGraph(Player player, String[] args) { List renderers = new ArrayList<>(); for (String arg : args) { GraphRenderer renderer = graphTypes.get(arg); if (renderer == null) { sendError(player, "Unknown graph type " + arg); return; } renderers.add(renderer); } if (renderers.size() > MAX_COMBINED) { sendError(player, "Too many graphs"); } else { CombinedGraph combinedGraph = new CombinedGraph(renderers.toArray(new GraphRenderer[0])); MapView view = installRenderer(player, combinedGraph); giveMap(player, view); } } private void giveMap(Player player, MapView mapView) { PlayerInventory inventory = player.getInventory(); ItemStack mapItem; if (LagUtils.isFilledMapSupported()) { mapItem = new ItemStack(Material.FILLED_MAP, 1); ItemMeta meta = mapItem.getItemMeta(); if (meta instanceof MapMeta) { MapMeta mapMeta = (MapMeta) meta; mapMeta.setMapView(mapView); mapItem.setItemMeta(meta); } } else { mapItem = new ItemStack(Material.MAP, 1, (short) mapView.getId()); } inventory.addItem(mapItem); player.sendMessage(ChatColor.DARK_GREEN + "You received a map with the graph"); } private MapView installRenderer(Player player, GraphRenderer graphType) { MapView mapView = Bukkit.createMap(player.getWorld()); mapView.getRenderers().forEach(mapView::removeRenderer); mapView.addRenderer(graphType); return mapView; } } ================================================ FILE: src/main/java/com/github/games647/lagmonitor/command/HelpCommand.java ================================================ package com.github.games647.lagmonitor.command; import com.github.games647.lagmonitor.LagMonitor; import java.util.Map; import java.util.Map.Entry; import net.md_5.bungee.api.ChatColor; import net.md_5.bungee.api.chat.ComponentBuilder; import net.md_5.bungee.api.chat.HoverEvent; import net.md_5.bungee.api.chat.HoverEvent.Action; import net.md_5.bungee.api.chat.TextComponent; import org.bukkit.command.Command; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; import org.bukkit.util.ChatPaginator; public class HelpCommand extends LagCommand { private static final int HOVER_MAX_LENGTH = 40; public HelpCommand(LagMonitor plugin) { super(plugin); } @Override public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { sender.sendMessage(ChatColor.AQUA + plugin.getName() + "-Help"); int maxWidth = ChatPaginator.GUARANTEED_NO_WRAP_CHAT_PAGE_WIDTH; if (!(sender instanceof Player)) { maxWidth = Integer.MAX_VALUE; } for (Entry> entry : plugin.getDescription().getCommands().entrySet()) { String commandKey = entry.getKey(); Map value = entry.getValue(); String description = ' ' + value.getOrDefault("description", "No description").toString(); String usage = ((String) value.getOrDefault("usage", '/' + commandKey)).replace("", commandKey); TextComponent component = createCommandHelp(usage, description, maxWidth); LagCommand.send(sender, component); } return true; } private TextComponent createCommandHelp(String usage, String description, int maxWidth) { TextComponent usageComponent = new TextComponent(usage); usageComponent.setColor(ChatColor.DARK_AQUA); TextComponent descriptionComponent = new TextComponent(description); descriptionComponent.setColor(ChatColor.GOLD); int totalLen = usage.length() + description.length(); if (totalLen > maxWidth) { int newDescLength = maxWidth - usage.length() - 3 - 1; if (newDescLength < 0) { newDescLength = 0; } String shortDesc = description.substring(0, newDescLength) + "..."; descriptionComponent.setText(shortDesc); ComponentBuilder hoverBuilder = new ComponentBuilder(""); String[] separated = ChatPaginator.wordWrap(description, HOVER_MAX_LENGTH); for (String line : separated) { hoverBuilder.append(line + '\n'); hoverBuilder.color(ChatColor.GOLD); } descriptionComponent.setHoverEvent(new HoverEvent(Action.SHOW_TEXT, hoverBuilder.create())); } else { descriptionComponent.setText(description); } usageComponent.addExtra(descriptionComponent); return usageComponent; } } ================================================ FILE: src/main/java/com/github/games647/lagmonitor/command/LagCommand.java ================================================ package com.github.games647.lagmonitor.command; import com.github.games647.lagmonitor.LagMonitor; import java.util.ArrayList; import java.util.Collection; import java.util.List; import net.md_5.bungee.api.chat.BaseComponent; import net.md_5.bungee.api.chat.TextComponent; import org.bukkit.ChatColor; import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandSender; import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.entity.Player; public abstract class LagCommand implements CommandExecutor { protected static final ChatColor PRIMARY_COLOR = ChatColor.DARK_AQUA; protected static final ChatColor SECONDARY_COLOR = ChatColor.GRAY; protected static final String NATIVE_NOT_FOUND = "Native library not found. Please download it and place it " + "inside configuration folder of this plugin to see this data"; protected final LagMonitor plugin; public LagCommand(LagMonitor plugin) { this.plugin = plugin; } private boolean isCommandAllowed(Command cmd, CommandSender sender) { if (!(sender instanceof Player)) { return true; } FileConfiguration config = plugin.getConfig(); Collection aliases = new ArrayList<>(cmd.getAliases()); aliases.add(cmd.getName()); for (String alias : aliases) { List aliasAllowed = config.getStringList("allow-" + alias); if (!aliasAllowed.isEmpty()) { return aliasAllowed.contains(sender.getName()); } } // allowlist doesn't exist return true; } public boolean canExecute(CommandSender sender, Command cmd) { if (!isCommandAllowed(cmd, sender)) { sendError(sender, "Command not allowed for you!"); return false; } return true; } protected void sendMessage(CommandSender sender, String title, String value) { sender.sendMessage(PRIMARY_COLOR + title + ": " + SECONDARY_COLOR + value); } protected void sendError(CommandSender sender, String msg) { sender.sendMessage(ChatColor.DARK_RED + msg); } public static void send(CommandSender sender, BaseComponent... components) { //CommandSender#sendMessage(BaseComponent[]) was introduced after 1.8. This is a backwards compatible solution if (sender instanceof Player) { sender.spigot().sendMessage(components); } else { sender.sendMessage(TextComponent.toLegacyText(components)); } } } ================================================ FILE: src/main/java/com/github/games647/lagmonitor/command/MbeanCommand.java ================================================ package com.github.games647.lagmonitor.command; import com.github.games647.lagmonitor.LagMonitor; import java.lang.management.ManagementFactory; import java.util.Arrays; import java.util.List; import java.util.Objects; import java.util.Set; import java.util.logging.Level; import java.util.stream.Stream; import javax.management.MBeanAttributeInfo; import javax.management.MBeanServer; import javax.management.ObjectInstance; import javax.management.ObjectName; import org.bukkit.ChatColor; import org.bukkit.command.Command; import org.bukkit.command.CommandSender; import org.bukkit.command.TabExecutor; import static java.util.stream.Collectors.toList; public class MbeanCommand extends LagCommand implements TabExecutor { public MbeanCommand(LagMonitor plugin) { super(plugin); } @Override public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { if (!canExecute(sender, command)) { return true; } MBeanServer mBeanServer = ManagementFactory.getPlatformMBeanServer(); if (args.length > 0) { try { ObjectName beanObject = ObjectName.getInstance(args[0]); if (args.length > 1) { Object result = mBeanServer.getAttribute(beanObject, args[1]); sender.sendMessage(ChatColor.DARK_GREEN + Objects.toString(result)); } else { MBeanAttributeInfo[] attributes = mBeanServer.getMBeanInfo(beanObject).getAttributes(); for (MBeanAttributeInfo attribute : attributes) { if ("ObjectName".equals(attribute.getName())) { //ignore the object name - it's already known if the user invoke the command continue; } sender.sendMessage(ChatColor.YELLOW + attribute.getName()); } } } catch (Exception ex) { plugin.getLogger().log(Level.SEVERE, null, ex); } } else { Set allBeans = mBeanServer.queryMBeans(null, null); allBeans.stream() .map(ObjectInstance::getObjectName) .map(ObjectName::getCanonicalName) .forEach(bean -> sender.sendMessage(ChatColor.DARK_AQUA + bean)); } return true; } @Override public List onTabComplete(CommandSender sender, Command command, String alias, String[] args) { String lastArg = args[args.length - 1]; MBeanServer mBeanServer = ManagementFactory.getPlatformMBeanServer(); Stream result = Stream.empty(); if (args.length == 1) { Set mbeans = mBeanServer.queryNames(null, null); result = mbeans.stream() .map(ObjectName::getCanonicalName) .filter(name -> name.startsWith(lastArg)); } else if (args.length == 2) { try { ObjectName beanObject = ObjectName.getInstance(args[0]); result = Arrays.stream(mBeanServer.getMBeanInfo(beanObject).getAttributes()) .map(MBeanAttributeInfo::getName) //ignore the object name - it's already known if the user invoke the command .filter(attribute -> !"ObjectName".equals(attribute)); } catch (Exception ex) { plugin.getLogger().log(Level.SEVERE, null, ex); } } return result.sorted(String.CASE_INSENSITIVE_ORDER).collect(toList()); } } ================================================ FILE: src/main/java/com/github/games647/lagmonitor/command/MonitorCommand.java ================================================ package com.github.games647.lagmonitor.command; import com.github.games647.lagmonitor.LagMonitor; import com.github.games647.lagmonitor.MethodMeasurement; import com.github.games647.lagmonitor.Pages; import com.github.games647.lagmonitor.task.MonitorTask; import com.google.common.base.Strings; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Timer; import java.util.concurrent.TimeUnit; import net.md_5.bungee.api.ChatColor; import net.md_5.bungee.api.chat.BaseComponent; import net.md_5.bungee.api.chat.ClickEvent; import net.md_5.bungee.api.chat.ClickEvent.Action; import net.md_5.bungee.api.chat.ComponentBuilder; import org.bukkit.Bukkit; import org.bukkit.command.Command; import org.bukkit.command.CommandSender; public class MonitorCommand extends LagCommand { public static final long SAMPLE_INTERVAL = 100L; public static final long SAMPLE_DELAY = TimeUnit.SECONDS.toMillis(1) / 2; private MonitorTask monitorTask; public MonitorCommand(LagMonitor plugin) { super(plugin); } @Override public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { if (!canExecute(sender, command)) { return true; } if (args.length > 0) { String monitorCommand = args[0].toLowerCase(); switch (monitorCommand) { case "start": startMonitor(sender); break; case "stop": stopMonitor(sender); break; case "paste": pasteMonitor(sender); break; default: sendError(sender, "Invalid command parameter"); } } else if (monitorTask == null) { sendError(sender, "Monitor is not running"); } else { List lines = new ArrayList<>(); synchronized (this) { MethodMeasurement rootSample = monitorTask.getRootSample(); printTrace(lines, 0, rootSample, 0); } Pages pagination = new Pages("Monitor", lines); pagination.send(sender); this.plugin.getPageManager().setPagination(sender.getName(), pagination); } return true; } private void printTrace(List lines, long parentTime, MethodMeasurement current, int depth) { String space = Strings.repeat(" ", depth); long currentTime = current.getTotalTime(); float timePercent = current.getTimePercent(parentTime); String clazz = Pages.filterPackageNames(current.getClassName()); String method = current.getMethod(); lines.add(new ComponentBuilder(space + "[-] ") .append(clazz + '.') .color(ChatColor.DARK_AQUA) .append(method) .color(ChatColor.DARK_GREEN) .append(' ' + timePercent + "%") .color(ChatColor.GRAY) .create()); Collection childInvokes = current.getChildInvokes().values(); List sortedList = new ArrayList<>(childInvokes); Collections.sort(sortedList); sortedList.forEach((child) -> printTrace(lines, currentTime, child, depth + 1)); } private void startMonitor(CommandSender sender) { Timer timer = plugin.getMonitorTimer(); if (monitorTask == null && timer == null) { timer = new Timer(plugin.getName() + "-Monitor"); plugin.setMonitorTimer(timer); monitorTask = new MonitorTask(plugin.getLogger(), Thread.currentThread().getId()); timer.scheduleAtFixedRate(monitorTask, SAMPLE_DELAY, SAMPLE_INTERVAL); sender.sendMessage(ChatColor.DARK_GREEN + "Monitor started"); } else { sendError(sender, "Monitor task is already running"); } } private void stopMonitor(CommandSender sender) { Timer timer = plugin.getMonitorTimer(); if (monitorTask == null && timer == null) { sendError(sender, "Monitor is not running"); } else { monitorTask = null; if (timer != null) { timer.cancel(); timer.purge(); plugin.setMonitorTimer(null); } sender.sendMessage(ChatColor.DARK_GREEN + "Monitor stopped"); } } private void pasteMonitor(final CommandSender sender) { Timer timer = plugin.getMonitorTimer(); if (monitorTask == null && timer == null) { sendError(sender, "Monitor is not running"); } Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { String reportUrl = monitorTask.paste(); if (reportUrl == null) { sendError(sender, "Error occurred. Please check the console"); } else { String profileUrl = reportUrl + ".profile"; send(sender, new ComponentBuilder("Report url: " + profileUrl) .color(ChatColor.GREEN) .event(new ClickEvent(Action.OPEN_URL, profileUrl)) .create()); } }); } } ================================================ FILE: src/main/java/com/github/games647/lagmonitor/command/NativeCommand.java ================================================ package com.github.games647.lagmonitor.command; import com.github.games647.lagmonitor.LagMonitor; import com.github.games647.lagmonitor.util.LagUtils; import java.time.Duration; import java.util.Arrays; import java.util.List; import java.util.Optional; import java.util.concurrent.TimeUnit; import oshi.SystemInfo; import oshi.demo.DetectVM; import oshi.hardware.Baseboard; import oshi.hardware.ComputerSystem; import oshi.hardware.Firmware; import oshi.hardware.HWDiskStore; import oshi.hardware.HardwareAbstractionLayer; import oshi.hardware.PhysicalMemory; import oshi.hardware.Sensors; import oshi.software.os.OSFileStore; import oshi.software.os.OperatingSystem; import org.bukkit.command.Command; import org.bukkit.command.CommandSender; public class NativeCommand extends LagCommand { public NativeCommand(LagMonitor plugin) { super(plugin); } @Override public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { if (!canExecute(sender, command)) { return true; } Optional optInfo = plugin.getNativeData().getSystemInfo(); if (optInfo.isPresent()) { displayNativeInfo(sender, optInfo.get()); } else { sendError(sender, NATIVE_NOT_FOUND); } return true; } private void displayNativeInfo(CommandSender sender, SystemInfo systemInfo) { HardwareAbstractionLayer hardware = systemInfo.getHardware(); OperatingSystem operatingSystem = systemInfo.getOperatingSystem(); //swap and load is already available in the environment command because MBeans already supports this long uptime = TimeUnit.SECONDS.toMillis(operatingSystem.getSystemUptime()); String uptimeFormat = LagMonitor.formatDuration(Duration.ofMillis(uptime)); sendMessage(sender, "OS Uptime", uptimeFormat); String startTime = LagMonitor.formatDuration(Duration.ofMillis(uptime)); sendMessage(sender, "OS Start time", startTime); sendMessage(sender, "CPU Freq", Arrays.toString(hardware.getProcessor().getCurrentFreq())); sendMessage(sender, "CPU Max Freq", String.valueOf(hardware.getProcessor().getMaxFreq())); sendMessage(sender, "VM Hypervisor", DetectVM.identifyVM()); //disk printDiskInfo(sender, hardware.getDiskStores()); displayMounts(sender, operatingSystem.getFileSystem().getFileStores()); printSensorsInfo(sender, hardware.getSensors()); printBoardInfo(sender, hardware.getComputerSystem()); printRAMInfo(sender, hardware.getMemory().getPhysicalMemory()); } private void printRAMInfo(CommandSender sender, List physicalMemories) { sender.sendMessage(PRIMARY_COLOR + "Memory:"); for (PhysicalMemory memory : physicalMemories) { sendMessage(sender, " Label", memory.getBankLabel()); sendMessage(sender, " Manufacturer", memory.getManufacturer()); sendMessage(sender, " Type", memory.getMemoryType()); sendMessage(sender, " Capacity", LagUtils.readableBytes(memory.getCapacity())); sendMessage(sender, " Clock speed", String.valueOf(memory.getClockSpeed())); } } private void printBoardInfo(CommandSender sender, ComputerSystem computerSystem) { sendMessage(sender, "System Manufacturer", computerSystem.getManufacturer()); sendMessage(sender, "System model", computerSystem.getModel()); sendMessage(sender, "Serial number", computerSystem.getSerialNumber()); sender.sendMessage(PRIMARY_COLOR + "Baseboard:"); Baseboard baseboard = computerSystem.getBaseboard(); sendMessage(sender, " Manufacturer", baseboard.getManufacturer()); sendMessage(sender, " Model", baseboard.getModel()); sendMessage(sender, " Serial", baseboard.getVersion()); sendMessage(sender, " Version", baseboard.getVersion()); sender.sendMessage(PRIMARY_COLOR + "BIOS Firmware:"); Firmware firmware = computerSystem.getFirmware(); sendMessage(sender, " Manufacturer", firmware.getManufacturer()); sendMessage(sender, " Name", firmware.getName()); sendMessage(sender, " Description", firmware.getDescription()); sendMessage(sender, " Version", firmware.getVersion()); sendMessage(sender, " Release date", firmware.getReleaseDate()); } private void printSensorsInfo(CommandSender sender, Sensors sensors) { double cpuTemperature = sensors.getCpuTemperature(); sendMessage(sender, "CPU Temp °C", String.valueOf(LagUtils.round(cpuTemperature))); sendMessage(sender, "Voltage", String.valueOf(LagUtils.round(sensors.getCpuVoltage()))); int[] fanSpeeds = sensors.getFanSpeeds(); sendMessage(sender, "Fan speed (rpm)", Arrays.toString(fanSpeeds)); } private void printDiskInfo(CommandSender sender, List diskStores) { //disk read write long diskReads = diskStores.stream().mapToLong(HWDiskStore::getReadBytes).sum(); long diskWrites = diskStores.stream().mapToLong(HWDiskStore::getWriteBytes).sum(); sendMessage(sender, "Disk read bytes", LagUtils.readableBytes(diskReads)); sendMessage(sender, "Disk write bytes", LagUtils.readableBytes(diskWrites)); sender.sendMessage(PRIMARY_COLOR + "Disks:"); for (HWDiskStore disk : diskStores) { String size = LagUtils.readableBytes(disk.getSize()); sendMessage(sender, " " + disk.getName(), disk.getModel() + ' ' + size); } } private void displayMounts(CommandSender sender, List fileStores) { sender.sendMessage(PRIMARY_COLOR + "Mounts:"); for (OSFileStore fileStore : fileStores) { printMountInfo(sender, fileStore); } } private void printMountInfo(CommandSender sender, OSFileStore fileStore) { String type = fileStore.getType(); String desc = fileStore.getDescription(); long totalSpaceBytes = fileStore.getTotalSpace(); String totalSpace = LagUtils.readableBytes(totalSpaceBytes); String usedSpace = LagUtils.readableBytes(totalSpaceBytes - fileStore.getUsableSpace()); String format = desc + ' ' + type + ' ' + usedSpace + '/' + totalSpace; sendMessage(sender, " " + fileStore.getMount(), format); } } ================================================ FILE: src/main/java/com/github/games647/lagmonitor/command/NetworkCommand.java ================================================ package com.github.games647.lagmonitor.command; import com.github.games647.lagmonitor.LagMonitor; import com.github.games647.lagmonitor.util.LagUtils; import java.util.Arrays; import java.util.Optional; import oshi.SystemInfo; import oshi.hardware.NetworkIF; import oshi.software.os.NetworkParams; import org.bukkit.command.Command; import org.bukkit.command.CommandSender; public class NetworkCommand extends LagCommand { public NetworkCommand(LagMonitor plugin) { super(plugin); } @Override public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { if (!canExecute(sender, command)) { return true; } Optional optInfo = plugin.getNativeData().getSystemInfo(); if (optInfo.isPresent()) { displayNetworkInfo(sender, optInfo.get()); } else { sender.sendMessage(NATIVE_NOT_FOUND); } return true; } private void displayNetworkInfo(CommandSender sender, SystemInfo systemInfo) { displayGlobalNetworkInfo(sender, systemInfo.getOperatingSystem().getNetworkParams()); for (NetworkIF networkInterface : systemInfo.getHardware().getNetworkIFs()) { displayInterfaceInfo(sender, networkInterface); } } private void displayGlobalNetworkInfo(CommandSender sender, NetworkParams networkParams) { sendMessage(sender, "Domain name", networkParams.getDomainName()); sendMessage(sender, "Host name", networkParams.getHostName()); sendMessage(sender, "Default IPv4 Gateway", networkParams.getIpv4DefaultGateway()); sendMessage(sender, "Default IPv6 Gateway", networkParams.getIpv6DefaultGateway()); sendMessage(sender, "DNS servers", Arrays.toString(networkParams.getDnsServers())); } private void displayInterfaceInfo(CommandSender sender, NetworkIF networkInterface) { sendMessage(sender, "Name", networkInterface.getName()); sendMessage(sender, " Display", networkInterface.getDisplayName()); sendMessage(sender, " MAC", networkInterface.getMacaddr()); sendMessage(sender, " MTU", String.valueOf(networkInterface.getMTU())); sendMessage(sender, " IPv4", Arrays.toString(networkInterface.getIPv4addr())); sendMessage(sender, " IPv6", Arrays.toString(networkInterface.getIPv6addr())); sendMessage(sender, " Speed", String.valueOf(networkInterface.getSpeed())); String receivedBytes = LagUtils.readableBytes(networkInterface.getBytesRecv()); String sentBytes = LagUtils.readableBytes(networkInterface.getBytesSent()); sendMessage(sender, " Received", receivedBytes); sendMessage(sender, " Sent", sentBytes); } } ================================================ FILE: src/main/java/com/github/games647/lagmonitor/command/PaginationCommand.java ================================================ package com.github.games647.lagmonitor.command; import com.github.games647.lagmonitor.LagMonitor; import com.github.games647.lagmonitor.Pages; import com.github.games647.lagmonitor.command.dump.DumpCommand; import com.google.common.primitives.Ints; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.Collections; import java.util.logging.Level; import net.md_5.bungee.api.chat.BaseComponent; import org.bukkit.ChatColor; import org.bukkit.command.Command; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; public class PaginationCommand extends DumpCommand { public PaginationCommand(LagMonitor plugin) { super(plugin, "pagination", "txt"); } @Override public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { if (!canExecute(sender, command)) { return true; } Pages pagination = plugin.getPageManager().getPagination(sender.getName()); if (pagination == null) { sendError(sender, "You have no pagination session"); return true; } if (args.length > 0) { String subCommand = args[0].toLowerCase(); switch (subCommand) { case "next": onNextPage(pagination, sender); break; case "prev": onPrevPage(pagination, sender); break; case "all": onShowAll(pagination, sender); break; case "save": onSave(pagination, sender); break; default: onPageNumber(subCommand, sender, pagination); } } else { sendError(sender, "Not enough arguments"); } return true; } private void onPageNumber(String subCommand, CommandSender sender, Pages pagination) { Integer page = Ints.tryParse(subCommand); if (page == null) { sendError(sender, "Unknown subcommand or not a valid page number"); } else { if (page < 1) { sendError(sender, "Page number too small"); return; } else if (page > pagination.getTotalPages(sender instanceof Player)) { sendError(sender, "Page number too high"); return; } pagination.send(sender, page); } } private void onNextPage(Pages pagination, CommandSender sender) { int lastPage = pagination.getLastSentPage(); if (lastPage >= pagination.getTotalPages(sender instanceof Player)) { sendError(sender,"You are already on the last page"); return; } pagination.send(sender, lastPage + 1); } private void onPrevPage(Pages pagination, CommandSender sender) { int lastPage = pagination.getLastSentPage(); if (lastPage <= 1) { sendError(sender,"You are already on the first page"); return; } pagination.send(sender, lastPage - 1); } private void onSave(Pages pagination, CommandSender sender) { StringBuilder lineBuilder = new StringBuilder(); for (BaseComponent[] line : pagination.getAllLines()) { for (BaseComponent component : line) { lineBuilder.append(component.toLegacyText()); } lineBuilder.append('\n'); } Path dumpFile = getNewDumpFile(); try { Files.write(dumpFile, Collections.singletonList(lineBuilder.toString())); sender.sendMessage(ChatColor.GRAY + "Dump created: " + dumpFile.getFileName()); } catch (IOException ex) { plugin.getLogger().log(Level.SEVERE, null, ex); } } private void onShowAll(Pages pagination, CommandSender sender) { if (sender instanceof Player) { Player player = (Player) sender; player.spigot().sendMessage(pagination.buildHeader(1, 1)); } else { BaseComponent[] header = pagination.buildHeader(1, 1); StringBuilder headerBuilder = new StringBuilder(); for (BaseComponent component : header) { headerBuilder.append(component.toLegacyText()); } sender.sendMessage(headerBuilder.toString()); } pagination.getAllLines().stream().map((line) -> { StringBuilder lineBuilder = new StringBuilder(); for (BaseComponent component : line) { lineBuilder.append(component.toLegacyText()); } return lineBuilder.toString(); }).forEach(sender::sendMessage); } } ================================================ FILE: src/main/java/com/github/games647/lagmonitor/command/StackTraceCommand.java ================================================ package com.github.games647.lagmonitor.command; import com.github.games647.lagmonitor.LagMonitor; import com.github.games647.lagmonitor.Pages; import java.lang.management.ManagementFactory; import java.lang.management.ThreadInfo; import java.lang.management.ThreadMXBean; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.stream.Collectors; import net.md_5.bungee.api.ChatColor; import net.md_5.bungee.api.chat.BaseComponent; import net.md_5.bungee.api.chat.ComponentBuilder; import org.bukkit.command.Command; import org.bukkit.command.CommandSender; import org.bukkit.command.TabExecutor; public class StackTraceCommand extends LagCommand implements TabExecutor { private static final int MAX_DEPTH = 75; public StackTraceCommand(LagMonitor plugin) { super(plugin); } @Override public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { if (!canExecute(sender, command)) { return true; } if (args.length > 0) { String threadName = args[0]; Map allStackTraces = Thread.getAllStackTraces(); for (Map.Entry entry : allStackTraces.entrySet()) { Thread thread = entry.getKey(); if (thread.getName().equalsIgnoreCase(threadName)) { StackTraceElement[] stackTrace = entry.getValue(); printStackTrace(sender, stackTrace); return true; } } sendError(sender, "No thread with that name found"); } else { ThreadMXBean threadBean = ManagementFactory.getThreadMXBean(); ThreadInfo threadInfo = threadBean.getThreadInfo(Thread.currentThread().getId(), MAX_DEPTH); printStackTrace(sender, threadInfo.getStackTrace()); } return true; } private void printStackTrace(CommandSender sender, StackTraceElement[] stackTrace) { List lines = new ArrayList<>(); //begin from the top for (int i = stackTrace.length - 1; i >= 0; i--) { lines.add(formatTraceElement(stackTrace[i])); } Pages pagination = new Pages("Stacktrace", lines); pagination.send(sender); plugin.getPageManager().setPagination(sender.getName(), pagination); } private BaseComponent[] formatTraceElement(StackTraceElement traceElement) { String className = Pages.filterPackageNames(traceElement.getClassName()); String methodName = traceElement.getMethodName(); boolean nativeMethod = traceElement.isNativeMethod(); int lineNumber = traceElement.getLineNumber(); String line = Integer.toString(lineNumber); if (nativeMethod) { line = "Native"; } return new ComponentBuilder(className + '.') .color(PRIMARY_COLOR.asBungee()) .append(methodName + ':') .color(ChatColor.DARK_GREEN) .append(line) .color(ChatColor.DARK_PURPLE) .create(); } @Override public List onTabComplete(CommandSender sender, Command command, String alias, String[] args) { List result = new ArrayList<>(); StringBuilder builder = new StringBuilder(); for (String arg : args) { builder.append(arg).append(' '); } String requestName = builder.toString(); ThreadInfo[] threads = ManagementFactory.getThreadMXBean().dumpAllThreads(false, false); return Arrays.stream(threads) .map(ThreadInfo::getThreadName) .filter(name -> name.startsWith(requestName)) .sorted(String.CASE_INSENSITIVE_ORDER) .collect(Collectors.toList()); } } ================================================ FILE: src/main/java/com/github/games647/lagmonitor/command/VmCommand.java ================================================ package com.github.games647.lagmonitor.command; import com.github.games647.lagmonitor.LagMonitor; import com.github.games647.lagmonitor.util.JavaVersion; import java.lang.management.ClassLoadingMXBean; import java.lang.management.CompilationMXBean; import java.lang.management.GarbageCollectorMXBean; import java.lang.management.ManagementFactory; import java.lang.management.RuntimeMXBean; import net.md_5.bungee.api.ChatColor; import net.md_5.bungee.api.chat.BaseComponent; import net.md_5.bungee.api.chat.ComponentBuilder; import net.md_5.bungee.api.chat.HoverEvent; import net.md_5.bungee.api.chat.HoverEvent.Action; import org.bukkit.command.Command; import org.bukkit.command.CommandSender; public class VmCommand extends LagCommand { public VmCommand(LagMonitor plugin) { super(plugin); } @Override public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { if (!canExecute(sender, command)) { return true; } //java version info displayJavaVersion(sender); //java paths sendMessage(sender, "Java lib", System.getProperty("sun.boot.library.path", "Unknown")); sendMessage(sender, "Java home", System.getProperty("java.home", "Unknown")); sendMessage(sender, "Temp path", System.getProperty("java.io.tmpdir", "Unknown")); displayRuntimeInfo(sender, ManagementFactory.getRuntimeMXBean()); displayCompilationInfo(sender, ManagementFactory.getCompilationMXBean()); displayClassLoading(sender, ManagementFactory.getClassLoadingMXBean()); //garbage collector for (GarbageCollectorMXBean collector : ManagementFactory.getGarbageCollectorMXBeans()) { displayCollectorStats(sender, collector); } return true; } private void displayCompilationInfo(CommandSender sender, CompilationMXBean compilationBean) { sendMessage(sender, "Compiler name", compilationBean.getName()); sendMessage(sender, "Compilation time (ms)", String.valueOf(compilationBean.getTotalCompilationTime())); } private void displayRuntimeInfo(CommandSender sender, RuntimeMXBean runtimeBean) { //vm sendMessage(sender, "Java VM", runtimeBean.getVmName() + ' ' + runtimeBean.getVmVersion()); sendMessage(sender, "Java vendor", runtimeBean.getVmVendor()); //vm specification sendMessage(sender, "Spec name", runtimeBean.getSpecName()); sendMessage(sender, "Spec vendor", runtimeBean.getSpecVendor()); sendMessage(sender, "Spec version", runtimeBean.getSpecVersion()); } private void displayCollectorStats(CommandSender sender, GarbageCollectorMXBean collector) { sendMessage(sender, "Garbage collector", collector.getName()); sendMessage(sender, " Count", String.valueOf(collector.getCollectionCount())); sendMessage(sender, " Time (ms)", String.valueOf(collector.getCollectionTime())); } private void displayClassLoading(CommandSender sender, ClassLoadingMXBean classBean) { sendMessage(sender, "Loaded classes", String.valueOf(classBean.getLoadedClassCount())); sendMessage(sender, "Total loaded", String.valueOf(classBean.getTotalLoadedClassCount())); sendMessage(sender, "Unloaded classes", String.valueOf(classBean.getUnloadedClassCount())); } private void displayJavaVersion(CommandSender sender) { JavaVersion currentVersion = JavaVersion.detect(); LagCommand.send(sender, formatJavaVersion(currentVersion)); sendMessage(sender, "Java release date", System.getProperty("java.version.date", "n/a")); sendMessage(sender, "Class version", System.getProperty("java.class.version")); } private BaseComponent[] formatJavaVersion(JavaVersion version) { ComponentBuilder builder = new ComponentBuilder("Java version: ").color(PRIMARY_COLOR.asBungee()) .append(version.getRaw()).color(SECONDARY_COLOR.asBungee()); if (version.isOutdated()) { builder = builder.append(" (").color(ChatColor.WHITE) .append("Outdated").color(ChatColor.DARK_RED) .event(new HoverEvent(Action.SHOW_TEXT, new ComponentBuilder("You're running an outdated Java version. \n" + "Java 9 and 10 are already released. \n" + "Newer versions could improve the performance or include bug or security fixes.") .color(ChatColor.DARK_AQUA).create())) .append(")").color(ChatColor.WHITE); } return builder.create(); } } ================================================ FILE: src/main/java/com/github/games647/lagmonitor/command/dump/DumpCommand.java ================================================ package com.github.games647.lagmonitor.command.dump; import com.github.games647.lagmonitor.LagMonitor; import com.github.games647.lagmonitor.command.LagCommand; import java.lang.management.ManagementFactory; import java.nio.file.Path; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import javax.management.InstanceNotFoundException; import javax.management.MBeanException; import javax.management.MBeanServer; import javax.management.MalformedObjectNameException; import javax.management.ObjectName; import javax.management.ReflectionException; public abstract class DumpCommand extends LagCommand { //https://docs.oracle.com/javase/10/docs/jre/api/management/extension/com/sun/management/DiagnosticCommandMBean.html protected static final String DIAGNOSTIC_BEAN = "com.sun.management:type=DiagnosticCommand"; protected static final String NOT_ORACLE_MSG = "You are not using Oracle JVM. OpenJDK hasn't implemented it yet"; private final String filePrefix; private final String fileExt; private final DateTimeFormatter dateFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd-HH-mm-ss"); public DumpCommand(LagMonitor plugin, String filePrefix, String fileExt) { super(plugin); this.filePrefix = filePrefix; this.fileExt = '.' + fileExt; } public Path getNewDumpFile() { String timeSuffix = '-' + LocalDateTime.now().format(dateFormat); Path folder = plugin.getDataFolder().toPath(); return folder.resolve(filePrefix + '-' + timeSuffix + fileExt); } public Object invokeBeanCommand(String beanName, String command, Object[] args, String[] signature) throws MalformedObjectNameException, MBeanException, InstanceNotFoundException, ReflectionException { MBeanServer beanServer = ManagementFactory.getPlatformMBeanServer(); ObjectName beanObject = ObjectName.getInstance(beanName); return beanServer.invoke(beanObject, command, args, signature); } public String invokeDiagnosticCommand(String command, String... args) throws MalformedObjectNameException, ReflectionException, MBeanException, InstanceNotFoundException { return (String) invokeBeanCommand(DIAGNOSTIC_BEAN, command, new Object[]{args}, new String[]{String[].class.getName()}); } } ================================================ FILE: src/main/java/com/github/games647/lagmonitor/command/dump/FlightCommand.java ================================================ package com.github.games647.lagmonitor.command.dump; import com.github.games647.lagmonitor.LagMonitor; import java.lang.management.ManagementFactory; import java.nio.file.Path; import java.util.Arrays; import java.util.logging.Level; import javax.management.InstanceNotFoundException; import javax.management.JMException; import javax.management.MBeanException; import javax.management.MBeanFeatureInfo; import javax.management.MBeanInfo; import javax.management.MBeanServer; import javax.management.MalformedObjectNameException; import javax.management.ObjectName; import javax.management.ReflectionException; import org.bukkit.command.Command; import org.bukkit.command.CommandSender; public class FlightCommand extends DumpCommand { private static final String START_COMMAND = "jfrStart"; private static final String STOP_COMMAND = "jfrStop"; private static final String DUMP_COMMAND = "jfrDump"; private static final String SETTINGS_FILE = "default.jfc"; private final String settingsPath; private final String recordingName; private final boolean isSupported; public FlightCommand(LagMonitor plugin) { super(plugin, "flight_recorder", "jfr"); this.recordingName = plugin.getName() + "-Record"; this.settingsPath = plugin.getDataFolder().toPath().resolve(SETTINGS_FILE).toAbsolutePath().toString(); isSupported = areFlightMethodsAvailable(); } private boolean areFlightMethodsAvailable() { MBeanServer beanServer = ManagementFactory.getPlatformMBeanServer(); try { ObjectName objectName = ObjectName.getInstance(DIAGNOSTIC_BEAN); MBeanInfo beanInfo = beanServer.getMBeanInfo(objectName); return Arrays.stream(beanInfo.getOperations()) .map(MBeanFeatureInfo::getName) .anyMatch(op -> op.contains("jfr")); } catch (JMException instanceNotFoundEx) { return false; } } @Override public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { if (!canExecute(sender, command)) { return true; } if (!isSupported) { sendError(sender, NOT_ORACLE_MSG); return true; } try { if (args.length > 0) { String subCommand = args[0].toLowerCase(); switch (subCommand) { case "start": onStartCommand(sender); break; case "stop": onStopCommand(sender); break; case "dump": onDumpCommand(sender); break; default: sendError(sender, "Unknown subcommand"); } } else { sendError(sender, "Not enough arguments"); } } catch (InstanceNotFoundException notFoundEx) { sendError(sender, NOT_ORACLE_MSG); } catch (Exception ex) { plugin.getLogger().log(Level.SEVERE, null, ex); sendError(sender, "An exception occurred. Please check the server log"); } return true; } private void onStartCommand(CommandSender sender) throws MalformedObjectNameException, ReflectionException, MBeanException, InstanceNotFoundException { String reply = invokeDiagnosticCommand(START_COMMAND, "settings=" + settingsPath, "name=" + recordingName); sender.sendMessage(reply); } private void onStopCommand(CommandSender sender) throws MalformedObjectNameException, ReflectionException, MBeanException, InstanceNotFoundException { String reply = invokeDiagnosticCommand(STOP_COMMAND, "name=" + recordingName); sender.sendMessage(reply); } private void onDumpCommand(CommandSender sender) throws MalformedObjectNameException, ReflectionException, MBeanException, InstanceNotFoundException { Path dumpFile = getNewDumpFile(); String reply = invokeDiagnosticCommand(DUMP_COMMAND , "filename=" + dumpFile.toAbsolutePath(), "name=" + recordingName); sender.sendMessage(reply); } } ================================================ FILE: src/main/java/com/github/games647/lagmonitor/command/dump/HeapCommand.java ================================================ package com.github.games647.lagmonitor.command.dump; import com.github.games647.lagmonitor.LagMonitor; import com.github.games647.lagmonitor.Pages; import com.sun.management.HotSpotDiagnosticMXBean; import java.lang.management.ManagementFactory; import java.nio.file.Path; import java.util.ArrayList; import java.util.List; import java.util.logging.Level; import javax.management.InstanceNotFoundException; import net.md_5.bungee.api.ChatColor; import net.md_5.bungee.api.chat.BaseComponent; import net.md_5.bungee.api.chat.ComponentBuilder; import org.bukkit.command.Command; import org.bukkit.command.CommandSender; public class HeapCommand extends DumpCommand { private static final String HEAP_COMMAND = "gcClassHistogram"; private static final boolean DUMP_DEAD_OBJECTS = false; private static final String[] EMPTY_STRING = {}; public HeapCommand(LagMonitor plugin) { super(plugin, "heap", "hprof"); } @Override public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { if (!canExecute(sender, command)) { return true; } if (args.length > 0) { String subCommand = args[0]; if ("dump".equalsIgnoreCase(subCommand)) { onDump(sender); } else { sendError(sender, "Unknown subcommand"); } return true; } List paginatedLines = new ArrayList<>(); try { String reply = invokeDiagnosticCommand(HEAP_COMMAND, EMPTY_STRING); for (String line : reply.split("\n")) { paginatedLines.add(new ComponentBuilder(line).create()); } Pages pagination = new Pages("Heap", paginatedLines); pagination.send(sender); plugin.getPageManager().setPagination(sender.getName(), pagination); } catch (InstanceNotFoundException instanceNotFoundException) { sendError(sender, NOT_ORACLE_MSG); } catch (Exception ex) { plugin.getLogger().log(Level.SEVERE, null, ex); sendError(sender, "An exception occurred. Please check the server log"); } return true; } private void onDump(CommandSender sender) { try { //test if this class is available Class.forName("com.sun.management.HotSpotDiagnosticMXBean"); //can be useful for dumping heaps in binary format HotSpotDiagnosticMXBean hostSpot = ManagementFactory.getPlatformMXBean(HotSpotDiagnosticMXBean.class); Path dumpFile = getNewDumpFile(); hostSpot.dumpHeap(dumpFile.toAbsolutePath().toString(), DUMP_DEAD_OBJECTS); sender.sendMessage(ChatColor.GRAY + "Dump created: " + dumpFile.getFileName()); sender.sendMessage(ChatColor.GRAY + "You can analyse it using VisualVM"); } catch (ClassNotFoundException notFoundEx) { sendError(sender, NOT_ORACLE_MSG); } catch (Exception ex) { plugin.getLogger().log(Level.SEVERE, null, ex); sendError(sender, "An exception occurred. Please check the server log"); } } } ================================================ FILE: src/main/java/com/github/games647/lagmonitor/command/dump/ThreadCommand.java ================================================ package com.github.games647.lagmonitor.command.dump; import com.github.games647.lagmonitor.LagMonitor; import com.github.games647.lagmonitor.Pages; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.logging.Level; import javax.management.InstanceNotFoundException; import net.md_5.bungee.api.ChatColor; import net.md_5.bungee.api.chat.BaseComponent; import net.md_5.bungee.api.chat.ClickEvent; import net.md_5.bungee.api.chat.ComponentBuilder; import net.md_5.bungee.api.chat.HoverEvent; import org.bukkit.command.Command; import org.bukkit.command.CommandSender; public class ThreadCommand extends DumpCommand { private static final String DUMP_COMMAND = "threadPrint"; private static final String[] EMPTY_STRING_ARRAY = {}; public ThreadCommand(LagMonitor plugin) { super(plugin, "thread", "tdump"); } @Override public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { if (!canExecute(sender, command)) { return true; } if (args.length > 0) { String subCommand = args[0]; if ("dump".equalsIgnoreCase(subCommand)) { onDump(sender); } else { sender.sendMessage(label); } return true; } List lines = new ArrayList<>(); Map allStackTraces = Thread.getAllStackTraces(); for (Thread thread : allStackTraces.keySet()) { if (thread.getContextClassLoader() == null) { //ignore java system threads like reference handler continue; } BaseComponent[] components = new ComponentBuilder("ID-" + thread.getId() + ": ") .color(PRIMARY_COLOR.asBungee()) .event(new ClickEvent(ClickEvent.Action.RUN_COMMAND , "/stacktrace " + thread.getName())) .event(new HoverEvent(HoverEvent.Action.SHOW_TEXT , new ComponentBuilder("Show the stacktrace").create())) .append(thread.getName() + ' ') .color(ChatColor.GOLD) .append(thread.getState().toString()) .color(SECONDARY_COLOR.asBungee()) .create(); lines.add(components); } Pages pagination = new Pages("Threads", lines); pagination.send(sender); plugin.getPageManager().setPagination(sender.getName(), pagination); return true; } private void onDump(CommandSender sender) { try { String result = invokeDiagnosticCommand(DUMP_COMMAND, EMPTY_STRING_ARRAY); Path dumpFile = getNewDumpFile(); Files.write(dumpFile, Collections.singletonList(result)); sender.sendMessage(ChatColor.GRAY + "Dump created: " + dumpFile.getFileName()); sender.sendMessage(ChatColor.GRAY + "You can analyse it using VisualVM"); } catch (InstanceNotFoundException instanceNotFoundException) { sendError(sender, NOT_ORACLE_MSG); } catch (Exception ex) { plugin.getLogger().log(Level.SEVERE, null, ex); sendError(sender, "An exception occurred. Please check the server log"); } } } ================================================ FILE: src/main/java/com/github/games647/lagmonitor/command/minecraft/PingCommand.java ================================================ package com.github.games647.lagmonitor.command.minecraft; import com.github.games647.lagmonitor.LagMonitor; import com.github.games647.lagmonitor.command.LagCommand; import com.github.games647.lagmonitor.util.LagUtils; import com.github.games647.lagmonitor.util.RollingOverHistory; import org.bukkit.Bukkit; import org.bukkit.ChatColor; import org.bukkit.command.Command; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; public class PingCommand extends LagCommand { public PingCommand(LagMonitor plugin) { super(plugin); } @Override public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { if (!canExecute(sender, command)) { return true; } if (args.length > 0) { displayPingOther(sender, command, args[0]); } else if (sender instanceof Player) { displayPingSelf(sender); } else { sendError(sender, "You have to be in game in order to see your own ping"); } return true; } private void displayPingSelf(CommandSender sender) { RollingOverHistory history = plugin.getPingManager().map(m -> m.getHistory(sender.getName())).orElse(null); if (history == null) { sendError(sender, "Sorry there is currently no data available"); return; } int lastPing = (int) history.getLastSample(); sender.sendMessage(PRIMARY_COLOR + "Your ping is: " + ChatColor.DARK_GREEN + lastPing + "ms"); float pingAverage = (float) (Math.round(history.getAverage() * 100.0) / 100.0); sender.sendMessage(PRIMARY_COLOR + "Average: " + ChatColor.DARK_GREEN + pingAverage + "ms"); } private void displayPingOther(CommandSender sender, Command command, String playerName) { if (sender.hasPermission(command.getPermission() + ".other")) { RollingOverHistory history = plugin.getPingManager().map(m -> m.getHistory(sender.getName())).orElse(null); if (history == null || !canSee(sender, playerName)) { sendError(sender, "No data for that player " + playerName); return; } int lastPing = (int) history.getLastSample(); sender.sendMessage(ChatColor.WHITE + playerName + PRIMARY_COLOR + "'s ping is: " + ChatColor.DARK_GREEN + lastPing + "ms"); float pingAverage = LagUtils.round(history.getAverage()); sender.sendMessage(PRIMARY_COLOR + "Average: " + ChatColor.DARK_GREEN + pingAverage + "ms"); } else { sendError(sender, "You don't have enough permission"); } } private boolean canSee(CommandSender sender, String playerName) { if (sender instanceof Player) { return ((Player) sender).canSee(Bukkit.getPlayerExact(playerName)); } return true; } } ================================================ FILE: src/main/java/com/github/games647/lagmonitor/command/minecraft/SystemCommand.java ================================================ package com.github.games647.lagmonitor.command.minecraft; import com.github.games647.lagmonitor.LagMonitor; import com.github.games647.lagmonitor.command.LagCommand; import com.github.games647.lagmonitor.traffic.TrafficReader; import com.github.games647.lagmonitor.util.LagUtils; import com.google.common.base.StandardSystemProperty; import java.io.File; import java.lang.management.ManagementFactory; import java.lang.management.RuntimeMXBean; import java.lang.management.ThreadMXBean; import java.time.Duration; import java.util.List; import java.util.Optional; import java.util.stream.Stream; import oshi.software.os.OSProcess; import org.bukkit.Bukkit; import org.bukkit.Chunk; import org.bukkit.World; import org.bukkit.command.Command; import org.bukkit.command.CommandSender; import org.bukkit.plugin.Plugin; import static com.github.games647.lagmonitor.util.LagUtils.readableBytes; public class SystemCommand extends LagCommand { public SystemCommand(LagMonitor plugin) { super(plugin); } @Override public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { if (!canExecute(sender, command)) { return true; } displayRuntimeInfo(sender, ManagementFactory.getRuntimeMXBean()); displayThreadInfo(sender, ManagementFactory.getThreadMXBean()); displayProcessInfo(sender); displayUserInfo(sender); displayMinecraftInfo(sender); return true; } private void displayUserInfo(CommandSender sender) { sender.sendMessage(PRIMARY_COLOR + "User"); sendMessage(sender, " Timezone", System.getProperty("user.timezone", "Unknown")); sendMessage(sender, " Country", System.getProperty("user.country", "Unknown")); sendMessage(sender, " Language", System.getProperty("user.language", "Unknown")); sendMessage(sender, " Home", StandardSystemProperty.USER_HOME.value()); sendMessage(sender, " Name", StandardSystemProperty.USER_NAME.value()); } private void displayProcessInfo(CommandSender sender) { sender.sendMessage(PRIMARY_COLOR + "Process:"); Optional optProcess = plugin.getNativeData().getProcess(); if (optProcess.isPresent()) { OSProcess process = optProcess.get(); sendMessage(sender, " PID", String.valueOf(process.getProcessID())); sendMessage(sender, " Name", process.getName()); sendMessage(sender, " Path", process.getPath()); sendMessage(sender, " Working directory", process.getCurrentWorkingDirectory()); sendMessage(sender, " User", process.getUser()); sendMessage(sender, " Group", process.getGroup()); } else { sendError(sender, NATIVE_NOT_FOUND); } } private void displayRuntimeInfo(CommandSender sender, RuntimeMXBean runtimeBean) { long uptime = runtimeBean.getUptime(); String uptimeFormat = LagMonitor.formatDuration(Duration.ofMillis(uptime)); displayMemoryInfo(sender, Runtime.getRuntime()); // runtime specific sendMessage(sender, "Uptime", uptimeFormat); sendMessage(sender, "Arguments", runtimeBean.getInputArguments().toString()); sendMessage(sender, "Classpath", runtimeBean.getClassPath()); sendMessage(sender, "Library path", runtimeBean.getLibraryPath()); } private void displayThreadInfo(CommandSender sender, ThreadMXBean threadBean) { sendMessage(sender, "Threads", String.valueOf(threadBean.getThreadCount())); sendMessage(sender, "Peak threads", String.valueOf(threadBean.getPeakThreadCount())); sendMessage(sender, "Daemon threads", String.valueOf(threadBean.getDaemonThreadCount())); sendMessage(sender, "Total started threads", String.valueOf(threadBean.getTotalStartedThreadCount())); } private void displayMemoryInfo(CommandSender sender, Runtime runtime) { long maxMemory = runtime.maxMemory(); long freeMemory = runtime.freeMemory(); long totalMemory = runtime.totalMemory(); sendMessage(sender, "Reserved used RAM", readableBytes(totalMemory - freeMemory)); sendMessage(sender, "Reserved free RAM", readableBytes(freeMemory)); sendMessage(sender, "Reserved RAM", readableBytes(totalMemory)); sendMessage(sender, "Max RAM", readableBytes(maxMemory)); } private void displayMinecraftInfo(CommandSender sender) { //Minecraft specific sendMessage(sender, "TPS", String.valueOf(plugin.getTpsHistoryTask().getLastSample())); TrafficReader trafficReader = plugin.getTrafficReader(); if (trafficReader != null) { String formattedIncoming = readableBytes(trafficReader.getIncomingBytes().longValue()); String formattedOutgoing = readableBytes(trafficReader.getOutgoingBytes().longValue()); sendMessage(sender, "Incoming Traffic", formattedIncoming); sendMessage(sender, "Outgoing Traffic", formattedOutgoing); } Plugin[] plugins = Bukkit.getPluginManager().getPlugins(); sendMessage(sender, "Loaded Plugins", String.format("%d/%d", getEnabledPlugins(plugins), plugins.length)); int onlinePlayers = Bukkit.getOnlinePlayers().size(); int maxPlayers = Bukkit.getMaxPlayers(); sendMessage(sender, "Players", String.format("%d/%d", onlinePlayers, maxPlayers)); displayWorldInfo(sender); sendMessage(sender, "Server version", Bukkit.getVersion()); } private void displayWorldInfo(CommandSender sender) { int entities = 0; int chunks = 0; int livingEntities = 0; int tileEntities = 0; long usedWorldSize = 0; List worlds = Bukkit.getWorlds(); for (World world : worlds) { for (Chunk loadedChunk : world.getLoadedChunks()) { tileEntities += loadedChunk.getTileEntities().length; } livingEntities += world.getLivingEntities().size(); entities += world.getEntities().size(); chunks += world.getLoadedChunks().length; File worldFolder = Bukkit.getWorld(world.getUID()).getWorldFolder(); usedWorldSize += LagUtils.getFolderSize(plugin.getLogger(), worldFolder.toPath()); } sendMessage(sender, "Entities", String.format("%d/%d", livingEntities, entities)); sendMessage(sender, "Tile Entities", String.valueOf(tileEntities)); sendMessage(sender, "Loaded Chunks", String.valueOf(chunks)); sendMessage(sender, "Worlds", String.valueOf(worlds.size())); sendMessage(sender, "World Size", readableBytes(usedWorldSize)); } private int getEnabledPlugins(Plugin[] plugins) { return (int) Stream.of(plugins).filter(Plugin::isEnabled).count(); } } ================================================ FILE: src/main/java/com/github/games647/lagmonitor/command/minecraft/TPSCommand.java ================================================ package com.github.games647.lagmonitor.command.minecraft; import com.github.games647.lagmonitor.LagMonitor; import com.github.games647.lagmonitor.command.LagCommand; import com.github.games647.lagmonitor.task.TPSHistoryTask; import com.google.common.collect.Lists; import java.text.DecimalFormat; import java.util.List; import java.util.stream.IntStream; import org.bukkit.ChatColor; import org.bukkit.command.Command; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; import org.bukkit.util.ChatPaginator; public class TPSCommand extends LagCommand { private static final ChatColor PRIMARY_COLOR = ChatColor.DARK_AQUA; private static final ChatColor SECONDARY_COLOR = ChatColor.GRAY; private static final char EMPTY_CHAR = ' '; private static final char GRAPH_CHAR = '+'; private static final char PLAYER_EMPTY_CHAR = '▂'; private static final char PLAYER_GRAPH_CHAR = '▇'; private static final int GRAPH_WIDTH = 60 / 2; private static final int GRAPH_LINES = ChatPaginator.CLOSED_CHAT_PAGE_HEIGHT - 3; public TPSCommand(LagMonitor plugin) { super(plugin); } @Override public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { if (!canExecute(sender, command)) { return true; } List graphLines = Lists.newArrayListWithExpectedSize(GRAPH_LINES); IntStream.rangeClosed(1, GRAPH_LINES) .map(i -> GRAPH_WIDTH * 2) .mapToObj(StringBuilder::new) .forEach(graphLines::add); TPSHistoryTask tpsHistoryTask = plugin.getTpsHistoryTask(); boolean console = true; if (sender instanceof Player) { console = false; } float[] lastSeconds = tpsHistoryTask.getMinuteSample().getSamples(); int position = tpsHistoryTask.getMinuteSample().getCurrentPosition(); buildGraph(lastSeconds, position, graphLines, console); graphLines.stream().map(Object::toString).forEach(sender::sendMessage); printAverageHistory(tpsHistoryTask, sender); sender.sendMessage(PRIMARY_COLOR + "Current TPS: " + tpsHistoryTask.getLastSample()); return true; } private void printAverageHistory(TPSHistoryTask tpsHistoryTask, CommandSender sender) { float minuteAverage = tpsHistoryTask.getMinuteSample().getAverage(); float quarterAverage = tpsHistoryTask.getQuarterSample().getAverage(); float halfHourAverage = tpsHistoryTask.getHalfHourSample().getAverage(); DecimalFormat formatter = new DecimalFormat("###.##"); sender.sendMessage(PRIMARY_COLOR + "Last Samples (1m, 15m, 30m): " + SECONDARY_COLOR + formatter.format(minuteAverage) + ' ' + formatter.format(quarterAverage) + ' ' + formatter.format(halfHourAverage)); } private void buildGraph(float[] lastSeconds, int lastPos, List graphLines, boolean console) { int index = lastPos; //in x-direction for (int xPos = 1; xPos < GRAPH_WIDTH; xPos++) { index++; if (index == lastSeconds.length) { index = 0; } float sampleSecond = lastSeconds[index]; buildLine(sampleSecond, graphLines, console); } } private void buildLine(float sampleSecond, List graphLines, boolean console) { ChatColor color = ChatColor.DARK_RED; int lines = 0; if (sampleSecond > 19.5F) { lines = GRAPH_LINES; color = ChatColor.DARK_GREEN; } else if (sampleSecond > 18.0F) { lines = GRAPH_LINES - 1; color = ChatColor.GREEN; } else if (sampleSecond > 17.0F) { lines = GRAPH_LINES - 2; color = ChatColor.YELLOW; } else if (sampleSecond > 15.0F) { lines = GRAPH_LINES - 3; color = ChatColor.GOLD; } else if (sampleSecond > 12.0F) { lines = GRAPH_LINES - 4; color = ChatColor.RED; } //in y-direction in reverse order for (int line = GRAPH_LINES - 1; line >= 0; line--) { if (lines == 0) { graphLines.get(line).append(ChatColor.WHITE).append(console ? EMPTY_CHAR : PLAYER_EMPTY_CHAR); continue; } lines--; graphLines.get(line).append(color).append(console ? GRAPH_CHAR : PLAYER_GRAPH_CHAR); } } } ================================================ FILE: src/main/java/com/github/games647/lagmonitor/command/minecraft/TasksCommand.java ================================================ package com.github.games647.lagmonitor.command.minecraft; import com.github.games647.lagmonitor.LagMonitor; import com.github.games647.lagmonitor.Pages; import com.github.games647.lagmonitor.command.LagCommand; import com.github.games647.lagmonitor.traffic.Reflection; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.util.ArrayList; import java.util.List; import net.md_5.bungee.api.chat.BaseComponent; import net.md_5.bungee.api.chat.ComponentBuilder; import org.bukkit.Bukkit; import org.bukkit.command.Command; import org.bukkit.command.CommandSender; import org.bukkit.plugin.Plugin; import org.bukkit.scheduler.BukkitTask; public class TasksCommand extends LagCommand { private static final MethodHandle taskHandle; static { Class taskClass = Reflection.getCraftBukkitClass("scheduler.CraftTask"); MethodHandle localHandle = null; try { localHandle = MethodHandles.publicLookup().findGetter(taskClass, "task", Runnable.class); } catch (NoSuchFieldException | IllegalAccessException noSuchFieldEx) { //ignore } taskHandle = localHandle; } public TasksCommand(LagMonitor plugin) { super(plugin); } @Override public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { if (!canExecute(sender, command)) { return true; } List lines = new ArrayList<>(); List pendingTasks = Bukkit.getScheduler().getPendingTasks(); for (BukkitTask pendingTask : pendingTasks) { lines.add(formatTask(pendingTask)); Class runnableClass = getRunnableClass(pendingTask); if (runnableClass != null) { lines.add(new ComponentBuilder(" Task: ") .color(PRIMARY_COLOR.asBungee()) .append(runnableClass.getSimpleName()) .color(SECONDARY_COLOR.asBungee()) .create()); } } Pages pagination = new Pages("Stacktrace", lines); pagination.send(sender); plugin.getPageManager().setPagination(sender.getName(), pagination); return true; } private BaseComponent[] formatTask(BukkitTask pendingTask) { Plugin owner = pendingTask.getOwner(); int taskId = pendingTask.getTaskId(); boolean sync = pendingTask.isSync(); String id = Integer.toString(taskId); if (sync) { id += "-Sync"; } else if (Bukkit.getScheduler().isCurrentlyRunning(taskId)) { id += "-Running"; } return new ComponentBuilder(owner.getName()) .color(PRIMARY_COLOR.asBungee()) .append('-' + id) .color(SECONDARY_COLOR.asBungee()) .create(); } private Class getRunnableClass(BukkitTask task) { try { return taskHandle.invokeExact(task).getClass(); } catch (Exception ex) { //ignore } catch (Throwable throwable) { throw (Error) throwable; } return null; } } ================================================ FILE: src/main/java/com/github/games647/lagmonitor/command/timing/PaperTimingsCommand.java ================================================ package com.github.games647.lagmonitor.command.timing; import co.aikar.timings.TimingHistory; import co.aikar.timings.Timings; import co.aikar.timings.TimingsManager; import com.github.games647.lagmonitor.LagMonitor; import com.github.games647.lagmonitor.Pages; import com.github.games647.lagmonitor.traffic.Reflection; import com.google.common.collect.EvictingQueue; import com.google.common.collect.Maps; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Map; import net.md_5.bungee.api.ChatColor; import net.md_5.bungee.api.chat.BaseComponent; import net.md_5.bungee.api.chat.ComponentBuilder; import net.md_5.bungee.api.chat.TextComponent; import org.bukkit.command.CommandSender; import org.bukkit.configuration.file.YamlConfiguration; import static com.github.games647.lagmonitor.util.LagUtils.round; /** * Paper and Sponge uses a new timings system (v2). * Missing data: * * TicksRecord * -> player ticks * -> timedTicks * -> entityTicks * -> activatedEntityTicks * -> tileEntityTicks * * MinuteReport * -> time * -> tps * -> avgPing * -> fullServerTick * -> ticks * * World data * -> worldName * -> tileEntities * -> entities * * => This concludes to the fact that the big benefits from Timings v2 isn't available. For example you cannot * scroll through your history */ public class PaperTimingsCommand extends TimingCommand { //TODO: Change to MethodHandles private static final String TIMINGS_PACKAGE = "co.aikar.timings"; private static final String EXPORT_CLASS = TIMINGS_PACKAGE + '.' + "TimingsExport"; private static final String HANDLER_CLASS = TIMINGS_PACKAGE + '.' + "TimingHandler"; private static final String HISTORY_ENTRY_CLASS = TIMINGS_PACKAGE + '.' + "TimingHistoryEntry"; private static final String DATA_CLASS = TIMINGS_PACKAGE + '.' + "TimingData"; private static final ChatColor HEADER_COLOR = ChatColor.YELLOW; private int historyInterval; public PaperTimingsCommand(LagMonitor plugin) { super(plugin); try { historyInterval = Reflection.getField("com.destroystokyo.paper.PaperConfig", "config" , YamlConfiguration.class).get(null).getInt("timings.history-interval"); } catch (IllegalArgumentException illegalArgumentException) { //cannot find paper spigot historyInterval = -1; } } @Override protected boolean isTimingsEnabled() { return Timings.isTimingsEnabled(); } @Override protected void sendTimings(CommandSender sender) { EvictingQueue history = Reflection.getField(TimingsManager.class, "HISTORY", EvictingQueue.class) .get(null); TimingHistory lastHistory = history.peek(); if (lastHistory == null) { sendError(sender, "Not enough data collected yet. You need to wait at least 5min after server startup"); return; } List lines = new ArrayList<>(); printTimings(lines, lastHistory); Pages pagination = new Pages("Paper Timings", lines); pagination.send(sender); plugin.getPageManager().setPagination(sender.getName(), pagination); } public void printTimings(Collection lines, TimingHistory lastHistory) { printHeadData(lastHistory, lines); Map idHandler = Maps.newHashMap(); Map groups = Reflection.getField(TIMINGS_PACKAGE + ".TimingIdentifier", "GROUP_MAP", Map.class).get(null); for (Object group : groups.values()) { String groupName = Reflection.getField(group.getClass(), "name", String.class).get(group); Iterable handlers = Reflection.getField(group.getClass(), "handlers", List.class).get(group); for (Object handler : handlers) { int id = Reflection.getField(HANDLER_CLASS, "id", Integer.TYPE).get(handler); Object identifier = Reflection.getField(HANDLER_CLASS, "identifier", Object.class).get(handler); String name = Reflection.getField(identifier.getClass(), "name", String.class).get(identifier); if (name.contains("Combined")) { idHandler.put(id, "Combined " + groupName); } else { idHandler.put(id, name); } } } //TimingHistoryEntry Object[] entries = Reflection.getField(TimingHistory.class, "entries", Object[].class).get(lastHistory); for (Object entry : entries) { Object parentData = Reflection.getField(HISTORY_ENTRY_CLASS, "data", Object.class).get(entry); int childId = Reflection.getField(DATA_CLASS, "id", Integer.TYPE).get(parentData); String handlerName = idHandler.get(childId); String parentName; if (handlerName == null) { parentName = "Unknown-" + childId; } else { parentName = handlerName; } int parentCount = Reflection.getField(DATA_CLASS, "count", Integer.TYPE).get(parentData); long parentTime = Reflection.getField(DATA_CLASS, "totalTime", Long.TYPE).get(parentData); // long parentLagCount = Reflection.getField(DATA_CLASS, "lagCount", Integer.TYPE).get(parentData); // long parentLagTime = Reflection.getField(DATA_CLASS, "lagTime", Long.TYPE).get(parentData); lines.add(new ComponentBuilder(parentName).color(HEADER_COLOR) .append(" Count: " + parentCount + " Time: " + parentTime).create()); Object[] children = Reflection.getField(HISTORY_ENTRY_CLASS, "children", Object[].class).get(entry); for (Object childData : children) { printChildren(parentData, childData, idHandler, lines); } } } private void printChildren(Object parent, Object childData, Map idMap, Collection lines) { int childId = Reflection.getField(DATA_CLASS, "id", Integer.TYPE).get(childData); String handlerName = idMap.get(childId); String childName; if (handlerName == null) { childName = "Unknown-" + childId; } else { childName = handlerName; } int childCount = Reflection.getField(DATA_CLASS, "count", Integer.TYPE).get(childData); long childTime = Reflection.getField(DATA_CLASS, "totalTime", Long.TYPE).get(childData); long parentTime = Reflection.getField(DATA_CLASS, "totalTime", Long.TYPE).get(parent); double percent = (double) childTime / parentTime; lines.add(new ComponentBuilder(" " + childName + " Count: " + childCount + " Time: " + childTime + ' ' + round(percent) + '%') .color(PRIMARY_COLOR.asBungee()).create()); } private void printHeadData(TimingHistory lastHistory, Collection lines) { // Represents all time spent running the server this history long totalTime = Reflection.getField(TimingHistory.class, "totalTime", Long.TYPE).get(lastHistory); long totalTicks = Reflection.getField(TimingHistory.class, "totalTicks", Long.TYPE).get(lastHistory); long cost = (long) Reflection.getMethod(EXPORT_CLASS, "getCost").invoke(null); lines.add(new ComponentBuilder("Cost: ") .color(PRIMARY_COLOR.asBungee()) .append(Long.toString(cost)).color(SECONDARY_COLOR.asBungee()).create()); double totalSeconds = (double) totalTime / 1000 / 1000; long playerTicks = TimingHistory.playerTicks; long tileEntityTicks = TimingHistory.tileEntityTicks; long activatedEntityTicks = TimingHistory.activatedEntityTicks; long entityTicks = TimingHistory.entityTicks; double activatedAvgEntities = (double) activatedEntityTicks / totalTicks; double totalAvgEntities = (double) entityTicks / totalTicks; double averagePlayers = (double) playerTicks / totalTicks; double desiredTicks = 20 * historyInterval; double averageTicks = totalTicks / desiredTicks * 20; String format = ChatColor.DARK_AQUA + "%s" + ' ' + ChatColor.GRAY + "%s"; //head data lines.add(TextComponent.fromLegacyText(String.format(format, "Total (sec):", round(totalSeconds)))); lines.add(TextComponent.fromLegacyText(String.format(format, "Ticks:", round(totalTicks)))); lines.add(TextComponent.fromLegacyText(String.format(format, "Avg ticks:", round(averageTicks)))); // lines.add(TextComponent.fromLegacyText(String.format(format, "Server Load:", round(serverLoad)))); lines.add(TextComponent.fromLegacyText(String.format(format, "AVG Players:", round(averagePlayers)))); lines.add(TextComponent.fromLegacyText(String.format(format, "Activated Entities:", round(activatedAvgEntities)) + " / " + round(totalAvgEntities))); } } ================================================ FILE: src/main/java/com/github/games647/lagmonitor/command/timing/SpigotTimingsCommand.java ================================================ package com.github.games647.lagmonitor.command.timing; import com.github.games647.lagmonitor.LagMonitor; import com.github.games647.lagmonitor.Pages; import com.github.games647.lagmonitor.traffic.Reflection; import com.github.games647.lagmonitor.traffic.Reflection.FieldAccessor; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Queue; import java.util.concurrent.TimeUnit; import net.md_5.bungee.api.ChatColor; import net.md_5.bungee.api.chat.BaseComponent; import net.md_5.bungee.api.chat.ComponentBuilder; import net.md_5.bungee.api.chat.TextComponent; import org.bukkit.Bukkit; import org.bukkit.command.CommandSender; import org.bukkit.command.defaults.TimingsCommand; import org.spigotmc.CustomTimingsHandler; import static com.github.games647.lagmonitor.util.LagUtils.round; /** * Parsed from the PHP project by aikar * https://github.com/aikar/timings */ public class SpigotTimingsCommand extends TimingCommand { //these timings will be in the breakdown report private static final String EXCLUDE_IDENTIFIER = "** "; //TODO: Change to MethodHandles public SpigotTimingsCommand(LagMonitor plugin) { super(plugin); } @Override protected boolean isTimingsEnabled() { return Bukkit.getPluginManager().useTimings(); } @Override protected void sendTimings(CommandSender sender) { //place sampleTime here to be very accurate long sampleTime = System.nanoTime() - TimingsCommand.timingStart; if (TimeUnit.NANOSECONDS.toMinutes(sampleTime) <= 5) { sendError(sender, "Sampling time is too low"); return; } Queue handlers = Reflection.getField(CustomTimingsHandler.class, "HANDLERS", Queue.class) .get(null); List lines = new ArrayList<>(); sendParsedOutput(handlers, lines, sampleTime); Pages pagination = new Pages("Paper Timings", lines); pagination.send(sender); this.plugin.getPageManager().setPagination(sender.getName(), pagination); } private void sendParsedOutput(Iterable handlers, Collection lines, long sampleTime) { Map timings = new HashMap<>(); Timing breakdownTiming = new Timing("Breakdown", -1, -1); Timing minecraftTiming = new Timing("Minecraft"); timings.put("Minecraft", minecraftTiming); timings.put("Breakdown", breakdownTiming); parseTimings(handlers, timings, minecraftTiming, breakdownTiming); long playerTicks = 0; long activatedEntityTicks = 0; long entityTicks = 0; long numTicks = 0; for (Map.Entry entry : breakdownTiming.getSubCategories().entrySet()) { String key = entry.getKey(); Timing value = entry.getValue(); if ("** tickEntity - EntityPlayer".equalsIgnoreCase(key)) { playerTicks = value.getTotalCount(); } else if ("** activatedTickEntity".equalsIgnoreCase(key)) { activatedEntityTicks = value.getTotalCount(); } else if ("** tickEntity".equalsIgnoreCase(key)) { entityTicks = value.getTotalCount(); } else if (key.contains(" - entityTick")) { numTicks = Math.max(numTicks, value.getTotalCount()); } } double serverLoad = 0; for (Map.Entry entry : timings.entrySet()) { String category = entry.getKey(); Timing timing = entry.getValue(); float pct = (float) timing.getTotalTime() / sampleTime * 100; String highlightedPercent = highlightPct(round(pct), 1, 3, 6); if (timing == minecraftTiming) { highlightedPercent = highlightPct(round(pct), 20, 40, 70); } //nanoseconds -> seconds float totalSeconds = (float) timing.getTotalTime() / 1000 / 1000 / 1000; lines.add(TextComponent.fromLegacyText(ChatColor.YELLOW + "=== " + category + " Total: " + round(totalSeconds) + "sec: " + highlightedPercent + "% " + ChatColor.YELLOW + "===")); if (timing.getSubCategories() != null) { for (Map.Entry subEntry : timing.getSubCategories().entrySet()) { String event = subEntry.getKey().replace("** ", "").replace("-", ""); int lastPackage = event.lastIndexOf('.'); if (lastPackage != -1) { event = event.substring(lastPackage + 1); } Timing subValue = subEntry.getValue(); double avg = subValue.calculateAverage(); double timesPerTick = (double) subValue.getTotalCount() / numTicks; if (timesPerTick > 1) { avg *= timesPerTick; } double pctTick = avg / 1000 / 1000 / 50 * 100; // float count = (float) subValue.getTotalCount() / 1000; //->ms avg = avg / 1000 / 1000; double pctTotal = (double) subValue.getTotalTime() / sampleTime * 100; if ("Full Server Tick".equalsIgnoreCase(event)) { serverLoad = pctTick; } lines.add(TextComponent.fromLegacyText(ChatColor.DARK_AQUA + event + ' ' + highlightPct(round(pctTotal), 10, 20, 50) + " Tick: " + highlightPct(round(pctTick), 3, 15, 40) + " AVG: " + round(avg) + "ms")); } } } lines.add(new ComponentBuilder("==========================================").color(ChatColor.GOLD).create()); long total = minecraftTiming.getTotalTime(); printHeadData(total, activatedEntityTicks, numTicks, entityTicks, playerTicks, sampleTime, lines, serverLoad); } private void printHeadData(long total, long activatedEntityTicks, long numTicks, long entityTicks, long playerTicks , long sampleTime, Collection lines, double serverLoad) { float totalSeconds = (float) total / 1000 / 1000 / 1000; float activatedAvgEntities = (float) activatedEntityTicks / numTicks; float totalAvgEntities = (float) entityTicks / numTicks; float averagePlayers = (float) playerTicks / numTicks; float desiredTicks = (float) sampleTime / 1000 / 1000 / 1000 * 20; float averageTicks = numTicks / desiredTicks * 20; String format = ChatColor.DARK_AQUA + "%s" + ' ' + ChatColor.GRAY + "%s"; //head data lines.add(TextComponent.fromLegacyText(String.format(format, "Total (sec):", round(totalSeconds)))); lines.add(TextComponent.fromLegacyText(String.format(format, "Ticks:", round(numTicks)))); lines.add(TextComponent.fromLegacyText(String.format(format, "Avg ticks:", round(averageTicks)))); lines.add(TextComponent.fromLegacyText(String.format(format, "Server Load:", round(serverLoad)))); lines.add(TextComponent.fromLegacyText(String.format(format, "AVG Players:", round(averagePlayers)))); lines.add(TextComponent.fromLegacyText(String.format(format, "Activated Entities:", round(activatedAvgEntities)) + " / " + round(totalAvgEntities))); //convert from nanoseconds to seconds String formatted = String.format(format, "Sample Time (sec):", round((float) sampleTime / 1000 / 1000 / 1000)); lines.add(TextComponent.fromLegacyText(formatted)); } private void parseTimings(Iterable handlers, Map timings , Timing minecraftTiming, Timing breakdownTiming) { // FieldAccessor getParent = Reflection // .getField(CustomTimingsHandler.class, "parent", CustomTimingsHandler.class); FieldAccessor getName = Reflection.getField(CustomTimingsHandler.class, "name", String.class); FieldAccessor getTotalTime = Reflection.getField(CustomTimingsHandler.class, "totalTime", Long.TYPE); FieldAccessor getCount = Reflection.getField(CustomTimingsHandler.class, "count", Long.TYPE); // FieldAccessor getViolations = Reflection.getField(CustomTimingsHandler.class, "violations", Long.TYPE); for (CustomTimingsHandler handler : handlers) { String subCategory = getName.get(handler); long totalTime = getTotalTime.get(handler); long count = getCount.get(handler); Timing active = minecraftTiming; if (subCategory.contains("Event: ")) { String pluginName = getProperty(subCategory, "Plugin"); subCategory = getProperty(subCategory, "Event"); active = timings.computeIfAbsent(pluginName, Timing::new); } else if (subCategory.contains("Task: ")) { String pluginName = getProperty(subCategory, "Task"); subCategory = getProperty(subCategory, "Runnable"); active = timings.computeIfAbsent(pluginName, Timing::new); } if (subCategory.startsWith(EXCLUDE_IDENTIFIER)) { breakdownTiming.addSubcategory(subCategory, totalTime, count); } else { active.addSubcategory(subCategory, totalTime, count); if (subCategory.startsWith("Task:")) { breakdownTiming.addSubcategory(EXCLUDE_IDENTIFIER + "Tasks", totalTime, count); } if (active.getTotalTime() >= 0) { active.addTotal(totalTime); } } } } private String getProperty(String line, String propertyName) { String categoryName = propertyName + ": "; int startIndex = line.indexOf(categoryName) + categoryName.length(); int endIndex = line.indexOf(' ', startIndex); if (endIndex == -1) { //line reached the end endIndex = line.length(); } return line.substring(startIndex, endIndex); } } ================================================ FILE: src/main/java/com/github/games647/lagmonitor/command/timing/Timing.java ================================================ package com.github.games647.lagmonitor.command.timing; import java.util.HashMap; import java.util.Map; import java.util.Objects; public class Timing implements Comparable { private final String category; private long totalTime; private long totalCount; private Map subcategories; public Timing(String category) { this.category = category; } public Timing(String category, long totalTime, long count) { this.category = category; this.totalTime = totalTime; this.totalCount = count; } public String getCategoryName() { return category; } public long getTotalTime() { return totalTime; } public void addTotal(long total) { this.totalTime += total; } public long getTotalCount() { return totalCount; } public void addCount(long count) { this.totalCount += count; } public double calculateAverage() { if (totalCount == 0) { return 0; } return (double) totalTime / totalCount; } public Map getSubCategories() { return subcategories; } public void addSubcategory(String name, long totalTime, long count) { if (subcategories == null) { //lazy creating subcategories = new HashMap<>(); } Timing timing = subcategories.computeIfAbsent(name, key -> new Timing(key, totalTime, count)); timing.addTotal(totalTime); timing.addCount(totalTime); } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Timing timing = (Timing) o; return totalTime == timing.totalTime && totalCount == timing.totalCount && Objects.equals(category, timing.category) && Objects.equals(subcategories, timing.subcategories); } @Override public int hashCode() { return Objects.hash(category, totalTime, totalCount, subcategories); } @Override public int compareTo(Timing other) { return Long.compare(totalTime, other.totalTime); } @Override public String toString() { return this.getClass().getSimpleName() + '{' + "category='" + category + '\'' + ", totalTime=" + totalTime + ", totalCount=" + totalCount + ", subcategories=" + subcategories + '}'; } } ================================================ FILE: src/main/java/com/github/games647/lagmonitor/command/timing/TimingCommand.java ================================================ package com.github.games647.lagmonitor.command.timing; import com.github.games647.lagmonitor.LagMonitor; import com.github.games647.lagmonitor.command.LagCommand; import net.md_5.bungee.api.ChatColor; import org.bukkit.command.Command; import org.bukkit.command.CommandSender; public abstract class TimingCommand extends LagCommand { public TimingCommand(LagMonitor plugin) { super(plugin); } @Override public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { if (!canExecute(sender, command)) { return true; } if (!isTimingsEnabled()) { sendError(sender,"The server deactivated timing reports"); sendError(sender,"Go to paper.yml or spigot.yml and activate timings"); return true; } sendTimings(sender); return true; } protected abstract void sendTimings(CommandSender sender); protected abstract boolean isTimingsEnabled(); protected String highlightPct(float percent, int low, int med, int high) { ChatColor prefix = ChatColor.GRAY; if (percent > high) { prefix = ChatColor.DARK_RED; } else if (percent > med) { prefix = ChatColor.GOLD; } else if (percent > low) { prefix = ChatColor.YELLOW; } return prefix + String.valueOf(percent) + '%' + ChatColor.GRAY; } } ================================================ FILE: src/main/java/com/github/games647/lagmonitor/graph/ClassesGraph.java ================================================ package com.github.games647.lagmonitor.graph; import java.lang.management.ClassLoadingMXBean; import java.lang.management.ManagementFactory; import org.bukkit.map.MapCanvas; public class ClassesGraph extends GraphRenderer { private final ClassLoadingMXBean classBean = ManagementFactory.getClassLoadingMXBean(); public ClassesGraph() { super("Classes"); } @Override public int renderGraphTick(MapCanvas canvas, int nextPosX) { int loadedClasses = classBean.getLoadedClassCount(); //round up to the nearest multiple of 5 int roundedMax = (int) (5 * (Math.ceil((float) loadedClasses / 5))); int loadedHeight = getHeightScaled(roundedMax, loadedClasses); fillBar(canvas, nextPosX, MAX_HEIGHT - loadedHeight, MAX_COLOR); //these is the max number return loadedClasses; } } ================================================ FILE: src/main/java/com/github/games647/lagmonitor/graph/CombinedGraph.java ================================================ package com.github.games647.lagmonitor.graph; import org.bukkit.map.MapCanvas; public class CombinedGraph extends GraphRenderer { private static final int SPACES = 2; private final GraphRenderer[] graphRenderers; private final int componentWidth; private final int[] componentLastPos; public CombinedGraph(GraphRenderer... renderers) { super("Combined"); this.graphRenderers = renderers; this.componentLastPos = new int[graphRenderers.length]; //MAX width - spaces between (length - 1) the components componentWidth = MAX_WIDTH - (SPACES * (graphRenderers.length - 1)) / graphRenderers.length; for (int i = 0; i < componentLastPos.length; i++) { componentLastPos[i] = i * componentWidth + i * SPACES; } } @Override public int renderGraphTick(MapCanvas canvas, int nextPosX) { for (int i = 0; i < graphRenderers.length; i++) { GraphRenderer graphRenderer = graphRenderers[i]; int position = this.componentLastPos[i]; position++; //index starts with 0 so in the end - 1 int maxComponentWidth = (i + 1) * componentWidth + i * SPACES - 1; if (position > maxComponentWidth) { //reset it to the start pos position = i * componentWidth + i * SPACES; } graphRenderer.renderGraphTick(canvas, position); this.componentLastPos[i] = position; } return 100; } } ================================================ FILE: src/main/java/com/github/games647/lagmonitor/graph/CpuGraph.java ================================================ package com.github.games647.lagmonitor.graph; import com.github.games647.lagmonitor.NativeManager; import org.bukkit.Bukkit; import org.bukkit.map.MapCanvas; import org.bukkit.plugin.Plugin; public class CpuGraph extends GraphRenderer { private final Plugin plugin; private final NativeManager nativeData; private final Object lock = new Object(); private int systemHeight; private int processHeight; public CpuGraph(Plugin plugin, NativeManager nativeData) { super("CPU Usage"); this.plugin = plugin; this.nativeData = nativeData; } @Override public int renderGraphTick(MapCanvas canvas, int nextPosX) { Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { int systemLoad = (int) (nativeData.getCPULoad() * 100); int processLOad = (int) (nativeData.getProcessCPULoad() * 100); int localSystemHeight = getHeightScaled(100, systemLoad); int localProcessHeight = getHeightScaled(100, processLOad); //flush updates synchronized (lock) { this.systemHeight = localSystemHeight; this.processHeight = localProcessHeight; } }); //read it only one time int localSystemHeight; int localProcessHeight; synchronized (lock) { localSystemHeight = this.systemHeight; localProcessHeight = this.processHeight; } fillBar(canvas, nextPosX, MAX_HEIGHT - localSystemHeight, MAX_COLOR); fillBar(canvas, nextPosX, MAX_HEIGHT - localProcessHeight, USED_COLOR); //set max height as 100% return 100; } } ================================================ FILE: src/main/java/com/github/games647/lagmonitor/graph/GraphRenderer.java ================================================ package com.github.games647.lagmonitor.graph; import org.bukkit.entity.Player; import org.bukkit.map.MapCanvas; import org.bukkit.map.MapPalette; import org.bukkit.map.MapRenderer; import org.bukkit.map.MapView; import org.bukkit.map.MinecraftFont; public abstract class GraphRenderer extends MapRenderer { protected static final int TEXT_HEIGHT = MinecraftFont.Font.getHeight(); //max height and width = 128 (index from 0-127) protected static final int MAX_WIDTH = 128; protected static final int MAX_HEIGHT = 128; //orange protected static final byte MAX_COLOR = MapPalette.matchColor(235, 171, 96); //blue protected static final byte USED_COLOR = MapPalette.matchColor(105, 182, 212); private int nextUpdate; private int nextPosX; private final String title; public GraphRenderer(String title) { this.title = title; } @Override public void render(MapView map, MapCanvas canvas, Player player) { if (nextUpdate <= 0) { //paint only every half seconds (20 Ticks / 2) nextUpdate = 10; if (nextPosX >= MAX_WIDTH) { //start again from the beginning nextPosX = 0; } clearBar(canvas, nextPosX); //make it more visual where the renderer is at the moment clearBar(canvas, nextPosX + 1); int maxValue = renderGraphTick(canvas, nextPosX); //override the color drawText(canvas, MAX_WIDTH / 2, MAX_HEIGHT / 2, title); //count indicators String maxText = Integer.toString(maxValue); drawText(canvas, MAX_WIDTH - Math.floorDiv(getTextWidth(maxText), 2), TEXT_HEIGHT, maxText); String midText = Integer.toString(maxValue / 2); drawText(canvas, MAX_WIDTH - Math.floorDiv(getTextWidth(midText), 2), MAX_HEIGHT / 2, midText); String zeroText = Integer.toString(0); drawText(canvas, MAX_WIDTH - Math.floorDiv(getTextWidth(zeroText), 2), MAX_HEIGHT, zeroText); nextPosX++; } nextUpdate--; } public abstract int renderGraphTick(MapCanvas canvas, int nextPosX); protected int getHeightScaled(int maxValue, int value) { return MAX_HEIGHT * value / maxValue; } protected void clearBar(MapCanvas canvas, int posX) { //resets the complete y coordinates on this x in order to free unused for (int yPos = 0; yPos < MAX_HEIGHT; yPos++) { canvas.setPixel(posX, yPos, (byte) 0); } } protected void clearMap(MapCanvas canvas) { for (int xPos = 0; xPos < MAX_WIDTH; xPos++) { fillBar(canvas, xPos, 0, (byte) 0); } } protected void fillBar(MapCanvas canvas, int xPos, int yStart, byte color) { for (int yPos = yStart; yPos < MAX_HEIGHT; yPos++) { canvas.setPixel(xPos, yPos, color); } } protected void drawText(MapCanvas canvas, int midX, int midY, String text) { int textWidth = getTextWidth(text); canvas.drawText(midX - (textWidth / 2), midY - (TEXT_HEIGHT / 2), MinecraftFont.Font, text); } private int getTextWidth(String text) { return MinecraftFont.Font.getWidth(text); } } ================================================ FILE: src/main/java/com/github/games647/lagmonitor/graph/HeapGraph.java ================================================ package com.github.games647.lagmonitor.graph; import java.lang.management.ManagementFactory; import java.lang.management.MemoryMXBean; import org.bukkit.map.MapCanvas; public class HeapGraph extends GraphRenderer { private final MemoryMXBean heapUsage = ManagementFactory.getMemoryMXBean(); public HeapGraph() { super("HeapUsage (MB)"); } @Override public int renderGraphTick(MapCanvas canvas, int nextPosX) { //byte -> mega byte int max = (int) (heapUsage.getHeapMemoryUsage().getCommitted() / 1024 / 1024); int used = (int) (heapUsage.getHeapMemoryUsage().getUsed() / 1024 / 1024); //round to the next 100 e.g. 801 -> 900 int roundedMax = ((max + 99) / 100) * 100; int maxHeight = getHeightScaled(roundedMax, max); int usedHeight = getHeightScaled(roundedMax, used); //x=0 y=0 is the left top point so convert it int convertedMaxHeight = MAX_HEIGHT - maxHeight; int convertedUsedHeight = MAX_HEIGHT - usedHeight; fillBar(canvas, nextPosX, convertedMaxHeight, MAX_COLOR); fillBar(canvas, nextPosX, convertedUsedHeight, USED_COLOR); return maxHeight; } } ================================================ FILE: src/main/java/com/github/games647/lagmonitor/graph/ThreadsGraph.java ================================================ package com.github.games647.lagmonitor.graph; import java.lang.management.ManagementFactory; import java.lang.management.ThreadMXBean; import org.bukkit.map.MapCanvas; public class ThreadsGraph extends GraphRenderer { private final ThreadMXBean threadBean = ManagementFactory.getThreadMXBean(); public ThreadsGraph() { super("Thread activity"); } @Override public int renderGraphTick(MapCanvas canvas, int nextPosX) { int threadCount = threadBean.getThreadCount(); int daemonCount = threadBean.getDaemonThreadCount(); //round up to the nearest multiple of 5 int roundedMax = (int) (5 * (Math.ceil((float) threadCount / 5))); int threadHeight = getHeightScaled(roundedMax, threadCount); int daemonHeight = getHeightScaled(roundedMax, daemonCount); fillBar(canvas, nextPosX, MAX_HEIGHT - threadHeight, MAX_COLOR); fillBar(canvas, nextPosX, MAX_HEIGHT - daemonHeight, USED_COLOR); //these is the max number of all threads return threadCount; } } ================================================ FILE: src/main/java/com/github/games647/lagmonitor/listener/BlockingConnectionSelector.java ================================================ package com.github.games647.lagmonitor.listener; import com.github.games647.lagmonitor.threading.BlockingActionManager; import com.github.games647.lagmonitor.threading.Injectable; import java.io.IOException; import java.net.Proxy; import java.net.ProxySelector; import java.net.SocketAddress; import java.net.URI; import java.util.Collections; import java.util.List; import java.util.regex.Pattern; public class BlockingConnectionSelector extends ProxySelector implements Injectable { private static final Pattern WWW_PATERN = Pattern.compile("www", Pattern.LITERAL); private final BlockingActionManager actionManager; private ProxySelector oldProxySelector; public BlockingConnectionSelector(BlockingActionManager actionManager) { this.actionManager = actionManager; } @Override public List select(URI uri) { String url = WWW_PATERN.matcher(uri.toString()).replaceAll(""); if (uri.getScheme().startsWith("http") || (uri.getPort() != 80 && uri.getPort() != 443)) { actionManager.checkBlockingAction("Socket: " + url); } return oldProxySelector == null ? Collections.singletonList(Proxy.NO_PROXY) : oldProxySelector.select(uri); } @Override public void connectFailed(URI uri, SocketAddress sa, IOException ioe) { if (oldProxySelector != null) { oldProxySelector.connectFailed(uri, sa, ioe); } } @Override public void inject() { ProxySelector proxySelector = ProxySelector.getDefault(); if (proxySelector != this) { oldProxySelector = proxySelector; ProxySelector.setDefault(this); } } @Override public void restore() { if (ProxySelector.getDefault() == this) { ProxySelector.setDefault(oldProxySelector); } } } ================================================ FILE: src/main/java/com/github/games647/lagmonitor/listener/GraphListener.java ================================================ package com.github.games647.lagmonitor.listener; import com.github.games647.lagmonitor.graph.GraphRenderer; import com.github.games647.lagmonitor.util.LagUtils; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; import org.bukkit.Bukkit; import org.bukkit.Material; import org.bukkit.entity.Item; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; import org.bukkit.event.player.PlayerDropItemEvent; import org.bukkit.event.player.PlayerInteractEvent; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.PlayerInventory; import org.bukkit.inventory.meta.ItemMeta; import org.bukkit.inventory.meta.MapMeta; import org.bukkit.map.MapView; public class GraphListener implements Listener { private final boolean mainHandSupported; public GraphListener() { boolean mainHandMethodEx = false; try { MethodType type = MethodType.methodType(ItemStack.class); MethodHandles.publicLookup().findVirtual(PlayerInventory.class, "getItemInMainHand", type); mainHandMethodEx = true; } catch (ReflectiveOperationException notFoundEx) { //default to false } this.mainHandSupported = mainHandMethodEx; } @EventHandler(ignoreCancelled = true, priority = EventPriority.HIGH) public void onInteract(PlayerInteractEvent clickEvent) { Player player = clickEvent.getPlayer(); PlayerInventory inventory = player.getInventory(); ItemStack mainHandItem; if (mainHandSupported) { mainHandItem = inventory.getItemInMainHand(); } else { mainHandItem = inventory.getItemInHand(); } if (isOurGraph(mainHandItem)) { inventory.setItemInMainHand(new ItemStack(Material.AIR)); } } @EventHandler(ignoreCancelled = true, priority = EventPriority.HIGH) public void onDrop(PlayerDropItemEvent dropItemEvent) { Item itemDrop = dropItemEvent.getItemDrop(); ItemStack mapItem = itemDrop.getItemStack(); if (isOurGraph(mapItem)) { mapItem.setAmount(0); } } private boolean isOurGraph(ItemStack item) { if (!LagUtils.isFilledMapSupported()) { return isOurGraphLegacy(item); } if (item.getType() != Material.FILLED_MAP) { return false; } ItemMeta meta = item.getItemMeta(); if (!(meta instanceof MapMeta)) { return false; } MapMeta mapMeta = (MapMeta) meta; MapView mapView = mapMeta.getMapView(); return mapView != null && isOurRenderer(mapView); } private boolean isOurGraphLegacy(ItemStack mapItem) { if (mapItem.getType() != Material.MAP) return false; short mapId = mapItem.getDurability(); MapView mapView = Bukkit.getMap(mapId); return mapView != null && isOurRenderer(mapView); } private boolean isOurRenderer(MapView mapView) { return mapView.getRenderers().stream() .anyMatch(GraphRenderer.class::isInstance); } } ================================================ FILE: src/main/java/com/github/games647/lagmonitor/listener/PageManager.java ================================================ package com.github.games647.lagmonitor.listener; import com.github.games647.lagmonitor.Pages; import java.util.HashMap; import java.util.Map; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; import org.bukkit.event.player.PlayerQuitEvent; public class PageManager implements Listener { private final Map pages = new HashMap<>(); @EventHandler public void onPlayerQuit(PlayerQuitEvent quitEvent) { pages.remove(quitEvent.getPlayer().getName()); } public Pages getPagination(String username) { return pages.get(username); } public void setPagination(String username, Pages pagination) { pages.put(username, pagination); } } ================================================ FILE: src/main/java/com/github/games647/lagmonitor/listener/ThreadSafetyListener.java ================================================ package com.github.games647.lagmonitor.listener; import com.github.games647.lagmonitor.threading.BlockingActionManager; import org.bukkit.event.Event; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; import org.bukkit.event.block.BlockFromToEvent; import org.bukkit.event.block.BlockPhysicsEvent; import org.bukkit.event.entity.CreatureSpawnEvent; import org.bukkit.event.entity.EntitySpawnEvent; import org.bukkit.event.entity.ItemSpawnEvent; import org.bukkit.event.inventory.InventoryOpenEvent; import org.bukkit.event.player.PlayerCommandPreprocessEvent; import org.bukkit.event.player.PlayerItemHeldEvent; import org.bukkit.event.player.PlayerJoinEvent; import org.bukkit.event.player.PlayerMoveEvent; import org.bukkit.event.player.PlayerQuitEvent; import org.bukkit.event.player.PlayerTeleportEvent; import org.bukkit.event.server.PluginDisableEvent; import org.bukkit.event.server.PluginEnableEvent; import org.bukkit.event.world.ChunkLoadEvent; import org.bukkit.event.world.ChunkUnloadEvent; import org.bukkit.event.world.SpawnChangeEvent; import org.bukkit.event.world.WorldLoadEvent; import org.bukkit.event.world.WorldSaveEvent; import org.bukkit.event.world.WorldUnloadEvent; /** * We can listen to events which are intended to run sync to the main thread. * If those events are fired on a async task the operation was likely not thread-safe. */ public class ThreadSafetyListener implements Listener { private final BlockingActionManager actionManager; public ThreadSafetyListener(BlockingActionManager actionManager) { this.actionManager = actionManager; } @EventHandler public void onCommand(PlayerCommandPreprocessEvent commandEvent) { checkSafety(commandEvent); } @EventHandler public void onInventoryOpen(InventoryOpenEvent inventoryOpenEvent) { checkSafety(inventoryOpenEvent); } @EventHandler public void onPlayerMove(PlayerMoveEvent moveEvent) { checkSafety(moveEvent); } @EventHandler public void onPlayerTeleport(PlayerTeleportEvent teleportEvent) { checkSafety(teleportEvent); } @EventHandler public void onPlayerJoin(PlayerJoinEvent joinEvent) { checkSafety(joinEvent); } @EventHandler public void onPlayerQuit(PlayerQuitEvent quitEvent) { checkSafety(quitEvent); } @EventHandler public void onItemHeldChange(PlayerItemHeldEvent itemHeldEvent) { checkSafety(itemHeldEvent); } @EventHandler public void onBlockPhysics(BlockPhysicsEvent blockPhysicsEvent) { checkSafety(blockPhysicsEvent); } @EventHandler public void onBlockFromTo(BlockFromToEvent blockFromToEvent) { checkSafety(blockFromToEvent); } @EventHandler public void onCreatureSpawn(CreatureSpawnEvent creatureSpawnEvent) { checkSafety(creatureSpawnEvent); } @EventHandler public void onItemSpawn(ItemSpawnEvent itemSpawnEvent) { checkSafety(itemSpawnEvent); } @EventHandler public void onChunkLoad(ChunkLoadEvent chunkLoadEvent) { checkSafety(chunkLoadEvent); } @EventHandler public void onChunkUnload(ChunkUnloadEvent chunkUnloadEvent) { checkSafety(chunkUnloadEvent); } @EventHandler public void onWorldLoad(WorldLoadEvent worldLoadEvent) { checkSafety(worldLoadEvent); } @EventHandler public void onWorldSave(WorldSaveEvent worldSaveEvent) { checkSafety(worldSaveEvent); } @EventHandler public void onWorldUnload(WorldUnloadEvent worldUnloadEvent) { checkSafety(worldUnloadEvent); } @EventHandler public void onPluginEnable(PluginEnableEvent pluginEnableEvent) { checkSafety(pluginEnableEvent); } @EventHandler public void onPluginDisable(PluginDisableEvent pluginDisableEvent) { checkSafety(pluginDisableEvent); } @EventHandler public void onSpawnChange(SpawnChangeEvent spawnChangeEvent) { checkSafety(spawnChangeEvent); } @EventHandler public void onSpawnChange(EntitySpawnEvent spawnEvent) { checkSafety(spawnEvent); } private void checkSafety(Event eventType) { //async executing of sync event String eventName = eventType.getEventName(); if (!eventType.isAsynchronous()) { actionManager.checkThreadSafety(eventName); } } } ================================================ FILE: src/main/java/com/github/games647/lagmonitor/logging/ForwardLogService.java ================================================ package com.github.games647.lagmonitor.logging; import org.slf4j.ILoggerFactory; import org.slf4j.IMarkerFactory; import org.slf4j.jul.JULServiceProvider; import org.slf4j.spi.MDCAdapter; import org.slf4j.spi.SLF4JServiceProvider; public class ForwardLogService implements SLF4JServiceProvider { private ILoggerFactory loggerFactory; private JULServiceProvider delegate; public ILoggerFactory getLoggerFactory() { return this.loggerFactory; } public IMarkerFactory getMarkerFactory() { return delegate.getMarkerFactory(); } public MDCAdapter getMDCAdapter() { return delegate.getMDCAdapter(); } @Override public String getRequesteApiVersion() { return delegate.getRequestedApiVersion(); } public String getRequestedApiVersion() { return delegate.getRequestedApiVersion(); } public void initialize() { this.delegate = new JULServiceProvider(); this.loggerFactory = new ForwardingLoggerFactory(); } } ================================================ FILE: src/main/java/com/github/games647/lagmonitor/logging/ForwardingLoggerFactory.java ================================================ package com.github.games647.lagmonitor.logging; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import org.slf4j.ILoggerFactory; import org.slf4j.Logger; import org.slf4j.jul.JDK14LoggerAdapter; public class ForwardingLoggerFactory implements ILoggerFactory { private final ConcurrentMap loggerMap = new ConcurrentHashMap<>(); public static java.util.logging.Logger PARENT_LOGGER; @Override public Logger getLogger(String name) { return loggerMap.computeIfAbsent(name, key -> { java.util.logging.Logger julLogger; if (PARENT_LOGGER == null) { julLogger = java.util.logging.Logger.getLogger(name); } else { julLogger = PARENT_LOGGER; } Logger newInstance = null; try { newInstance = createJDKLogger(julLogger); } catch (NoSuchMethodException | InvocationTargetException | InstantiationException | IllegalAccessException e) { e.printStackTrace(); System.out.println("Failed to created logging instance"); } return newInstance; }); } protected static Logger createJDKLogger(java.util.logging.Logger parent) throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException { Class adapterClass = JDK14LoggerAdapter.class; Constructor cons = adapterClass.getDeclaredConstructor(java.util.logging.Logger.class); cons.setAccessible(true); return (Logger) cons.newInstance(parent); } } ================================================ FILE: src/main/java/com/github/games647/lagmonitor/ping/PaperPing.java ================================================ package com.github.games647.lagmonitor.ping; import org.bukkit.entity.Player; public class PaperPing implements PingFetcher { @Override public boolean isAvailable() { try { //Only available in Paper Player.Spigot.class.getDeclaredMethod("getPing"); return true; } catch (NoSuchMethodException noSuchMethodEx) { return false; } } @Override public int getPing(Player player) { return player.spigot().getPing(); } } ================================================ FILE: src/main/java/com/github/games647/lagmonitor/ping/PingFetcher.java ================================================ package com.github.games647.lagmonitor.ping; import org.bukkit.entity.Player; public interface PingFetcher { boolean isAvailable(); int getPing(Player player); } ================================================ FILE: src/main/java/com/github/games647/lagmonitor/ping/ReflectionPing.java ================================================ package com.github.games647.lagmonitor.ping; import com.github.games647.lagmonitor.LagMonitor; import com.github.games647.lagmonitor.traffic.Reflection; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodHandles.Lookup; import java.lang.invoke.MethodType; import java.util.logging.Level; import java.util.logging.Logger; import org.bukkit.entity.Player; import org.bukkit.plugin.java.JavaPlugin; public class ReflectionPing implements PingFetcher { private static final MethodHandle pingFromPlayerHandle; static { MethodHandle localPing = null; Class craftPlayerClass = Reflection.getCraftBukkitClass("entity.CraftPlayer"); String playerClazz = "EntityPlayer"; Class entityPlayer = Reflection.getMinecraftClass(playerClazz, "level." + playerClazz); Lookup lookup = MethodHandles.publicLookup(); try { MethodType type = MethodType.methodType(entityPlayer); MethodHandle getHandle = lookup.findVirtual(craftPlayerClass, "getHandle", type); MethodHandle pingField = lookup.findGetter(entityPlayer, "ping", Integer.TYPE); // combine the handles to invoke it only once // *getPing(getHandle*) -> add the result of getHandle to the next getPing call // a call to this handle will get the ping from a player instance localPing = MethodHandles.collectArguments(pingField, 0, getHandle) // allow interface with invokeExact .asType(MethodType.methodType(int.class, Player.class)); } catch (NoSuchMethodException | IllegalAccessException | NoSuchFieldException reflectiveEx) { Logger logger = JavaPlugin.getPlugin(LagMonitor.class).getLogger(); logger.log(Level.WARNING, "Cannot find ping field/method", reflectiveEx); } pingFromPlayerHandle = localPing; } @Override public boolean isAvailable() { return pingFromPlayerHandle != null; } @Override public int getPing(Player player) { try { return (int) pingFromPlayerHandle.invokeExact(player); } catch (Exception ex) { return -1; } catch (Throwable throwable) { throw (Error) throwable; } } } ================================================ FILE: src/main/java/com/github/games647/lagmonitor/ping/SpigotPing.java ================================================ package com.github.games647.lagmonitor.ping; import org.bukkit.entity.Player; public class SpigotPing implements PingFetcher { @Override public boolean isAvailable() { try { //Only available in Paper Player.class.getDeclaredMethod("getPing"); return true; } catch (NoSuchMethodException noSuchMethodEx) { return false; } } @Override public int getPing(Player player) { return player.getPing(); } } ================================================ FILE: src/main/java/com/github/games647/lagmonitor/storage/MonitorSaveTask.java ================================================ package com.github.games647.lagmonitor.storage; import com.github.games647.lagmonitor.LagMonitor; import com.github.games647.lagmonitor.NativeManager; import com.github.games647.lagmonitor.util.LagUtils; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import java.lang.management.ManagementFactory; import java.lang.management.OperatingSystemMXBean; import java.nio.file.Path; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.UUID; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.logging.Level; import org.bukkit.Bukkit; import org.bukkit.World; import org.bukkit.entity.Player; import static com.github.games647.lagmonitor.util.LagUtils.round; public class MonitorSaveTask implements Runnable { protected final LagMonitor plugin; protected final Storage storage; public MonitorSaveTask(LagMonitor plugin, Storage storage) { this.plugin = plugin; this.storage = storage; } @Override public void run() { try { int monitorId = save(); if (monitorId == -1) { //error occurred return; } Map worldsData = getWorldData(); if (!storage.saveWorlds(monitorId, worldsData.values())) { //error occurred return; } List playerData = getPlayerData(worldsData); storage.savePlayers(playerData); } catch (ExecutionException | InterruptedException ex) { plugin.getLogger().log(Level.SEVERE, "Error saving monitoring data", ex); } } private List getPlayerData(final Map worldsData) throws InterruptedException, ExecutionException { Future> playerFuture = Bukkit.getScheduler() .callSyncMethod(plugin, () -> { Collection onlinePlayers = Bukkit.getOnlinePlayers(); List playerData = Lists.newArrayListWithCapacity(onlinePlayers.size()); for (Player player : onlinePlayers) { UUID worldId = player.getWorld().getUID(); int worldRowId = 0; WorldData worldData = worldsData.get(worldId); if (worldData != null) { worldRowId = worldData.getRowId(); } String name = player.getName(); int lastPing = plugin.getPingManager().map(m -> ((int) m.getHistory(name).getLastSample())) .orElse(-1); UUID playerId = player.getUniqueId(); playerData.add(new PlayerData(worldRowId, playerId, name, lastPing)); } return playerData; }); return playerFuture.get(); } private Map getWorldData() throws ExecutionException, InterruptedException { //this is not thread-safe and have to run sync Future> worldFuture = Bukkit.getScheduler() .callSyncMethod(plugin, () -> { List worlds = Bukkit.getWorlds(); Map worldsData = Maps.newHashMapWithExpectedSize(worlds.size()); for (World world : worlds) { worldsData.put(world.getUID(), WorldData.fromWorld(world)); } return worldsData; }); Map worldsData = worldFuture.get(); //this can run async because it's thread-safe worldsData.values().parallelStream() .forEach(data -> { Path worldFolder = Bukkit.getWorld(data.getWorldName()).getWorldFolder().toPath(); int worldSize = LagUtils.byteToMega(LagUtils.getFolderSize(plugin.getLogger(), worldFolder)); data.setWorldSize(worldSize); }); return worldsData; } private int save() { Runtime runtime = Runtime.getRuntime(); int maxMemory = LagUtils.byteToMega(runtime.maxMemory()); //we need the free ram not the free heap int usedRam = LagUtils.byteToMega(runtime.totalMemory() - runtime.freeMemory()); int freeRam = maxMemory - usedRam; float freeRamPct = round((freeRam * 100) / maxMemory, 4); OperatingSystemMXBean osBean = ManagementFactory.getOperatingSystemMXBean(); float loadAvg = round(osBean.getSystemLoadAverage(), 4); if (loadAvg < 0) { //windows doesn't support this loadAvg = 0; } NativeManager nativeData = plugin.getNativeData(); float systemUsage = round(nativeData.getCPULoad() * 100, 4); float processUsage = round(nativeData.getProcessCPULoad() * 100, 4); int totalOsMemory = LagUtils.byteToMega(nativeData.getTotalMemory()); int freeOsRam = LagUtils.byteToMega(nativeData.getFreeMemory()); float freeOsRamPct = round((freeOsRam * 100) / totalOsMemory, 4); return storage.saveMonitor(processUsage, systemUsage, freeRam, freeRamPct, freeOsRam, freeOsRamPct, loadAvg); } } ================================================ FILE: src/main/java/com/github/games647/lagmonitor/storage/NativeSaveTask.java ================================================ package com.github.games647.lagmonitor.storage; import com.github.games647.lagmonitor.LagMonitor; import com.github.games647.lagmonitor.traffic.TrafficReader; import com.github.games647.lagmonitor.util.LagUtils; import java.nio.file.Path; import java.nio.file.Paths; import java.time.Duration; import java.time.Instant; import java.util.List; import java.util.Optional; import oshi.SystemInfo; import oshi.hardware.NetworkIF; import oshi.software.os.OSProcess; import static com.github.games647.lagmonitor.util.LagUtils.round; public class NativeSaveTask implements Runnable { private final LagMonitor plugin; private final Storage storage; private Instant lastCheck = Instant.now(); private int lastMcRead; private int lastMcWrite; private int lastDiskRead; private int lastDiskWrite; private int lastNetRead; private int lastNetWrite; public NativeSaveTask(LagMonitor plugin, Storage storage) { this.plugin = plugin; this.storage = storage; } @Override public void run() { Instant currentTime = Instant.now(); int timeDiff = (int) Duration.between(lastCheck, currentTime).getSeconds(); int mcReadDiff = 0; int mcWriteDiff = 0; TrafficReader trafficReader = plugin.getTrafficReader(); if (trafficReader != null) { int mcRead = LagUtils.byteToMega(trafficReader.getIncomingBytes().longValue()); mcReadDiff = getDifference(mcRead, lastMcRead, timeDiff); lastMcRead = mcRead; int mcWrite = LagUtils.byteToMega(trafficReader.getOutgoingBytes().longValue()); mcWriteDiff = getDifference(mcWrite, lastMcWrite, timeDiff); lastMcWrite = mcWrite; } int totalSpace = LagUtils.byteToMega(plugin.getNativeData().getTotalSpace()); int freeSpace = LagUtils.byteToMega(plugin.getNativeData().getFreeSpace()); //4 decimal places -> Example: 0.2456 float freeSpacePct = round((freeSpace * 100 / (float) totalSpace), 4); int diskReadDiff = 0; int diskWriteDiff = 0; int netReadDiff = 0; int netWriteDiff = 0; Optional systemInfo = plugin.getNativeData().getSystemInfo(); if (systemInfo.isPresent()) { List networkIfs = systemInfo.get().getHardware().getNetworkIFs(); if (!networkIfs.isEmpty()) { NetworkIF networkInterface = networkIfs.get(0); int netRead = LagUtils.byteToMega(networkInterface.getBytesRecv()); netReadDiff = getDifference(netRead, lastNetRead, timeDiff); lastNetRead = netRead; int netWrite = LagUtils.byteToMega(networkInterface.getBytesSent()); netWriteDiff = getDifference(netWrite, lastNetWrite, timeDiff); lastNetWrite = netWrite; } Path root = Paths.get(".").getRoot(); Optional optProcess = plugin.getNativeData().getProcess(); if (root != null && optProcess.isPresent()) { OSProcess process = optProcess.get(); String rootFileSystem = root.toAbsolutePath().toString(); int diskRead = LagUtils.byteToMega(process.getBytesRead()); diskReadDiff = getDifference(diskRead, lastDiskRead, timeDiff); lastDiskRead = diskRead; int diskWrite = LagUtils.byteToMega(process.getBytesWritten()); diskWriteDiff = getDifference(diskWrite, lastDiskWrite, timeDiff); lastDiskWrite = diskWrite; } } lastCheck = currentTime; storage.saveNative(mcReadDiff, mcWriteDiff, freeSpace, freeSpacePct, diskReadDiff, diskWriteDiff , netReadDiff, netWriteDiff); } private int getDifference(long newVal, long oldVal, long timeDiff) { return (int) ((newVal - oldVal) / timeDiff); } } ================================================ FILE: src/main/java/com/github/games647/lagmonitor/storage/PlayerData.java ================================================ package com.github.games647.lagmonitor.storage; import java.util.UUID; public class PlayerData { private final int worldId; private final UUID uuid; private final String playerName; private final int ping; public PlayerData(int worldId, UUID uuid, String playerName, int ping) { this.worldId = worldId; this.uuid = uuid; this.playerName = playerName; if (ping < 0) { this.ping = Integer.MAX_VALUE; } else { this.ping = ping; } } public int getWorldId() { return worldId; } public UUID getUuid() { return uuid; } public String getPlayerName() { return playerName; } public int getPing() { return ping; } @Override public String toString() { return this.getClass().getSimpleName() + '{' + "worldId=" + worldId + ", uuid=" + uuid + ", playerName=" + playerName + ", ping=" + ping + '}'; } } ================================================ FILE: src/main/java/com/github/games647/lagmonitor/storage/Storage.java ================================================ package com.github.games647.lagmonitor.storage; import com.google.common.collect.Lists; import com.mysql.cj.jdbc.MysqlDataSource; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.Collection; import java.util.logging.Level; import java.util.logging.Logger; public class Storage { private static final String TPS_TABLE = "tps"; private static final String PLAYERS_TABLE = "players"; private static final String MONITOR_TABLE = "monitor"; private static final String WORLDS_TABLE = "worlds"; private static final String NATIVE_TABLE = "native"; private final MysqlDataSource dataSource; private final Logger logger; private final String prefix; public Storage(Logger logger, String host, int port, String database, boolean usessl, String user, String pass, String prefix) { this.logger = logger; this.prefix = prefix; this.dataSource = new MysqlDataSource(); this.dataSource.setUser(user); this.dataSource.setPassword(pass); this.dataSource.setServerName(host); this.dataSource.setPort(port); this.dataSource.setDatabaseName(database); tryFeature(dataSource, source -> source.setUseSSL(usessl), "Failed to configure use ssl - using to default"); tryFeature(dataSource, source -> source.setCachePrepStmts(true), "Failed to enable caching of statements"); tryFeature(dataSource, source -> source.setUseServerPrepStmts(true), "Failed to enable server caching"); } @FunctionalInterface private interface FeatureTester { void run(MysqlDataSource dataSource) throws SQLException; } private void tryFeature(MysqlDataSource dataSource, FeatureTester task, String errorMsg) { try { task.run(dataSource); } catch (SQLException sqlEx) { this.logger.log(Level.WARNING, errorMsg, sqlEx); } } public void createTables() throws SQLException { try (InputStream in = getClass().getResourceAsStream("/create.sql"); BufferedReader reader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8)); Connection con = dataSource.getConnection(); Statement stmt = con.createStatement()) { StringBuilder builder = new StringBuilder(); String line; while ((line = reader.readLine()) != null) { if (line.startsWith("#")) continue; builder.append(line); if (line.endsWith(";")) { stmt.addBatch(builder.toString().replace("{prefix}", prefix)); builder = new StringBuilder(); } } stmt.executeBatch(); } catch (IOException ioEx) { logger.log(Level.SEVERE, "Failed to load migration file", ioEx); } } public int saveMonitor(float procUsage, float osUsage, int freeRam, float freeRamPct, int osRam, float osRamPct , float loadAvg) { try (Connection con = dataSource.getConnection(); PreparedStatement stmt = con.prepareStatement("INSERT INTO " + prefix + MONITOR_TABLE + " (process_usage, os_usage, free_ram, free_ram_pct, os_free_ram, os_free_ram_pct, load_avg)" + " VALUES (?, ?, ?, ?, ?, ?, ?)", Statement.RETURN_GENERATED_KEYS)) { stmt.setFloat(1, procUsage); stmt.setFloat(2, osUsage); stmt.setInt(3, freeRam); stmt.setFloat(4, freeRamPct); stmt.setInt(5, osRam); stmt.setFloat(6, osRamPct); stmt.setFloat(7, loadAvg); stmt.execute(); try (ResultSet generatedKeys = stmt.getGeneratedKeys()) { if (generatedKeys.next()) { return generatedKeys.getInt(1); } } } catch (SQLException sqlEx) { logger.log(Level.SEVERE, "Error saving monitor data to database", sqlEx); logger.log(Level.SEVERE, "Using this data {0}" , Lists.newArrayList(procUsage, osUsage, freeRam, freeRamPct, osRam, osRamPct, loadAvg)); } return -1; } public boolean saveWorlds(int monitorId, Collection worldsData) { if (worldsData.isEmpty()) { return false; } try (Connection con = dataSource.getConnection(); PreparedStatement stmt = con.prepareStatement("INSERT INTO " + prefix + WORLDS_TABLE + " (monitor_id, world_name, chunks_loaded, tile_entities, entities, world_size)" + " VALUES (?, ?, ?, ?, ?, ?)", Statement.RETURN_GENERATED_KEYS)) { for (WorldData worldData : worldsData) { stmt.setInt(1, monitorId); stmt.setString(2, worldData.getWorldName()); stmt.setInt(3, worldData.getLoadedChunks()); stmt.setInt(4, worldData.getTileEntities()); stmt.setInt(5, worldData.getEntities()); stmt.setInt(6, worldData.getWorldSize()); stmt.addBatch(); } stmt.executeBatch(); try (ResultSet generatedKeys = stmt.getGeneratedKeys()) { for (WorldData worldData : worldsData) { if (generatedKeys.next()) { worldData.setRowId(generatedKeys.getInt(1)); } } } return true; } catch (SQLException sqlEx) { logger.log(Level.SEVERE, "Error saving worlds data to database", sqlEx); logger.log(Level.SEVERE, "Using this data {0}", worldsData); } return false; } public void savePlayers(Collection playerData) { if (playerData.isEmpty()) { return; } try (Connection con = dataSource.getConnection(); PreparedStatement stmt = con.prepareStatement("INSERT INTO " + prefix + PLAYERS_TABLE + " (world_id, uuid, name, ping) " + "VALUES (?, ?, ?, ?)")) { for (PlayerData data : playerData) { stmt.setInt(1, data.getWorldId()); stmt.setString(2, data.getUuid().toString()); stmt.setString(3, data.getPlayerName()); stmt.setInt(4, data.getPing()); stmt.addBatch(); } stmt.executeBatch(); } catch (SQLException sqlEx) { logger.log(Level.SEVERE, "Error saving player data to database", sqlEx); logger.log(Level.SEVERE, "Using this data {0}", playerData); } } public void saveNative(int mcRead, int mcWrite, int freeSpace, float freePct, int diskRead, int diskWrite , int netRead, int netWrite) { try (Connection con = dataSource.getConnection(); PreparedStatement stmt = con.prepareStatement("INSERT INTO " + prefix + NATIVE_TABLE + " (mc_read, mc_write, free_space, free_space_pct, disk_read, disk_write, net_read, net_write)" + " VALUES (?, ?, ?, ?, ?, ?, ?, ?)")) { stmt.setInt(1, mcRead); stmt.setInt(2, mcWrite); stmt.setInt(3, freeSpace); stmt.setFloat(4, freePct); stmt.setInt(5, diskRead); stmt.setInt(6, diskWrite); stmt.setInt(7, netRead); stmt.setInt(8, netWrite); stmt.execute(); } catch (SQLException sqlEx) { logger.log(Level.SEVERE, "Error saving native stats to database", sqlEx); logger.log(Level.SEVERE, "Using this data {0}" , Lists.newArrayList(mcRead, mcWrite, freeSpace, freePct, diskRead, diskWrite, netRead, netWrite)); } } public void saveTps(float tps) { try (Connection con = dataSource.getConnection(); PreparedStatement stmt = con.prepareStatement("INSERT INTO " + prefix + TPS_TABLE + " (tps) VALUES (?)")) { stmt.setFloat(1, tps); stmt.execute(); } catch (SQLException sqlEx) { logger.log(Level.SEVERE, "Error saving tps to database", sqlEx); logger.log(Level.SEVERE, "Using this data {0}", new Object[]{tps}); } } } ================================================ FILE: src/main/java/com/github/games647/lagmonitor/storage/TPSSaveTask.java ================================================ package com.github.games647.lagmonitor.storage; import com.github.games647.lagmonitor.task.TPSHistoryTask; public class TPSSaveTask implements Runnable { private final TPSHistoryTask tpsHistoryTask; private final Storage storage; public TPSSaveTask(TPSHistoryTask tpsHistoryTask, Storage storage) { this.tpsHistoryTask = tpsHistoryTask; this.storage = storage; } @Override public void run() { float lastSample = tpsHistoryTask.getLastSample(); if (lastSample > 0 && lastSample < 50) { storage.saveTps(lastSample); } } } ================================================ FILE: src/main/java/com/github/games647/lagmonitor/storage/WorldData.java ================================================ package com.github.games647.lagmonitor.storage; import org.bukkit.Chunk; import org.bukkit.World; public class WorldData { private final String worldName; private final int loadedChunks; private final int tileEntities; private final int entities; private int worldSize; private int rowId; public static WorldData fromWorld(World world) { String worldName = world.getName(); int tileEntities = 0; for (Chunk loadedChunk : world.getLoadedChunks()) { tileEntities += loadedChunk.getTileEntities().length; } int entities = world.getEntities().size(); int chunks = world.getLoadedChunks().length; return new WorldData(worldName, chunks, tileEntities, entities); } public WorldData(String worldName, int loadedChunks, int tileEntities, int entities) { this.worldName = worldName; this.loadedChunks = loadedChunks; this.tileEntities = tileEntities; this.entities = entities; } public String getWorldName() { return worldName; } public int getLoadedChunks() { return loadedChunks; } public int getTileEntities() { return tileEntities; } public int getEntities() { return entities; } public int getWorldSize() { return worldSize; } public void setWorldSize(int worldSize) { this.worldSize = worldSize; } public int getRowId() { return rowId; } public void setRowId(int rowId) { this.rowId = rowId; } @Override public String toString() { return this.getClass().getSimpleName() + '{' + "worldName=" + worldName + ", loadedChunks=" + loadedChunks + ", tileEntities=" + tileEntities + ", entities=" + entities + ", worldSize=" + worldSize + ", rowId=" + rowId + '}'; } } ================================================ FILE: src/main/java/com/github/games647/lagmonitor/task/IODetectorTask.java ================================================ package com.github.games647.lagmonitor.task; import com.github.games647.lagmonitor.threading.BlockingActionManager; import java.lang.Thread.State; import java.util.TimerTask; public class IODetectorTask extends TimerTask { private final BlockingActionManager actionManager; private final Thread mainThread; public IODetectorTask(BlockingActionManager actionManager, Thread mainThread) { this.actionManager = actionManager; this.mainThread = mainThread; } @Override public void run() { //According to this post the thread is still in Runnable although it's waiting for //file/http resources //https://stackoverflow.com/questions/20795295/why-jstack-out-says-thread-state-is-runnable-while-socketread if (mainThread.getState() == State.RUNNABLE) { //Based on this post we have to check the top element of the stack //https://stackoverflow.com/questions/20891386/how-to-detect-thread-being-blocked-by-io StackTraceElement[] stackTrace = mainThread.getStackTrace(); StackTraceElement topElement = stackTrace[stackTrace.length - 1]; if (topElement.isNativeMethod()) { //Socket/SQL (connect) - java.net.DualStackPlainSocketImpl.connect0 //Socket/SQL (read) - java.net.SocketInputStream.socketRead0 //Socket/SQL (write) - java.net.SocketOutputStream.socketWrite0 if (isElementEqual(topElement, "java.net.DualStackPlainSocketImpl", "connect0") || isElementEqual(topElement, "java.net.SocketInputStream", "socketRead0") || isElementEqual(topElement, "java.net.SocketOutputStream", "socketWrite0")) { actionManager.logCurrentStack("Server is performing {1} on the main thread. " + "Properly caused by {0}", "java.net.SocketStream"); } //File (in) - java.io.FileInputStream.readBytes //File (out) - java.io.FileOutputStream.writeBytes else if (isElementEqual(topElement, "java.io.FileInputStream", "readBytes") || isElementEqual(topElement, "java.io.FileOutputStream", "writeBytes")) { actionManager.logCurrentStack("Server is performing {1} on the main thread. " + "Properly caused by {0}", "java.io.FileStream"); } } } } private boolean isElementEqual(StackTraceElement traceElement, String className, String methodName) { return traceElement.getClassName().equals(className) && traceElement.getMethodName().equals(methodName); } } ================================================ FILE: src/main/java/com/github/games647/lagmonitor/task/MonitorTask.java ================================================ package com.github.games647.lagmonitor.task; import com.github.games647.lagmonitor.MethodMeasurement; import com.github.games647.lagmonitor.command.MonitorCommand; import com.google.common.net.UrlEscapers; import com.google.gson.Gson; import com.google.gson.JsonObject; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.io.Reader; import java.lang.management.ManagementFactory; import java.lang.management.ThreadInfo; import java.lang.management.ThreadMXBean; import java.net.HttpURLConnection; import java.net.URL; import java.nio.charset.StandardCharsets; import java.util.TimerTask; import java.util.logging.Level; import java.util.logging.Logger; /** * Based on the project https://github.com/sk89q/WarmRoast by sk89q */ public class MonitorTask extends TimerTask { private static final String PASTE_URL = "https://paste.enginehub.org/paste"; private static final int MAX_DEPTH = 25; private final ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean(); private final Logger logger; private final long threadId; private MethodMeasurement rootNode; private int samples; public MonitorTask(Logger logger, long threadId) { this.logger = logger; this.threadId = threadId; } public synchronized MethodMeasurement getRootSample() { return rootNode; } public synchronized int getSamples() { return samples; } @Override public void run() { ThreadInfo threadInfo = threadMXBean.getThreadInfo(threadId, MAX_DEPTH); StackTraceElement[] stackTrace = threadInfo.getStackTrace(); if (stackTrace.length > 0) { StackTraceElement rootElement = stackTrace[stackTrace.length - 1]; synchronized (this) { samples++; if (rootNode == null) { String rootClass = rootElement.getClassName(); String rootMethod = rootElement.getMethodName(); String id = rootClass + '.' + rootMethod; rootNode = new MethodMeasurement(id, rootClass, rootMethod); } rootNode.onMeasurement(stackTrace, 0, MonitorCommand.SAMPLE_INTERVAL); } } } public String paste() { try { HttpURLConnection httpConnection = (HttpURLConnection) new URL(PASTE_URL).openConnection(); httpConnection.setRequestMethod("POST"); httpConnection.setDoOutput(true); httpConnection.setDoInput(true); try (BufferedWriter writer = new BufferedWriter( new OutputStreamWriter(httpConnection.getOutputStream(), StandardCharsets.UTF_8)) ) { writer.write("content=" + UrlEscapers.urlPathSegmentEscaper().escape(toString())); writer.write("&from=" + logger.getName()); } JsonObject object; try (Reader reader = new BufferedReader( new InputStreamReader(httpConnection.getInputStream(), StandardCharsets.UTF_8)) ) { object = new Gson().fromJson(reader, JsonObject.class); } if (object.has("url")) { return object.get("url").getAsString(); } logger.log(Level.INFO, "Failed to parse url from {0}", object); } catch (IOException ex) { logger.log(Level.SEVERE, "Failed to upload monitoring data", ex); } return null; } @Override public String toString() { ThreadInfo threadInfo = threadMXBean.getThreadInfo(threadId, MAX_DEPTH); StringBuilder builder = new StringBuilder(); builder.append(threadInfo.getThreadName()); builder.append(' '); synchronized (this) { builder.append(rootNode.getTotalTime()).append("ms"); builder.append('\n'); rootNode.writeString(builder, 1); } return builder.toString(); } } ================================================ FILE: src/main/java/com/github/games647/lagmonitor/task/PingManager.java ================================================ package com.github.games647.lagmonitor.task; import com.github.games647.lagmonitor.ping.PaperPing; import com.github.games647.lagmonitor.ping.PingFetcher; import com.github.games647.lagmonitor.ping.ReflectionPing; import com.github.games647.lagmonitor.ping.SpigotPing; import com.github.games647.lagmonitor.util.RollingOverHistory; import com.google.common.collect.Lists; import java.lang.reflect.InvocationTargetException; import java.util.HashMap; import java.util.List; import java.util.Map; import org.bukkit.Bukkit; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; import org.bukkit.event.player.PlayerJoinEvent; import org.bukkit.event.player.PlayerQuitEvent; import org.bukkit.plugin.Plugin; public class PingManager implements Runnable, Listener { //the server is pinging the client every 40 Ticks (2 sec) - so check it then //https://github.com/bergerkiller/CraftSource/blob/master/net.minecraft.server/PlayerConnection.java#L178 public static final int PING_INTERVAL = 2 * 20; private static final int SAMPLE_SIZE = 5; private final Map playerHistory = new HashMap<>(); private final Plugin plugin; private final PingFetcher pingFetcher; public PingManager(Plugin plugin) throws ReflectiveOperationException { this.pingFetcher = initializePingFetchur(); this.plugin = plugin; } private PingFetcher initializePingFetchur() throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException { List> fetchurs = Lists.newArrayList( SpigotPing.class, PaperPing.class, ReflectionPing.class ); for (Class fetchurClass : fetchurs) { PingFetcher fetchur = fetchurClass.getDeclaredConstructor().newInstance(); if (fetchur.isAvailable()) return fetchur; } throw new NoSuchMethodException("No valid ping fetcher found"); } @Override public void run() { playerHistory.forEach((playerName, history) -> { Player player = Bukkit.getPlayerExact(playerName); if (player != null) { int ping = pingFetcher.getPing(player); history.add(ping); } }); } public RollingOverHistory getHistory(String playerName) { return playerHistory.get(playerName); } public void addPlayer(Player player) { int ping = pingFetcher.getPing(player); playerHistory.put(player.getName(), new RollingOverHistory(SAMPLE_SIZE, ping)); } public void removePlayer(Player player) { playerHistory.remove(player.getName()); } @EventHandler public void onPlayerJoin(PlayerJoinEvent joinEvent) { Player player = joinEvent.getPlayer(); Bukkit.getScheduler().runTaskLater(plugin, () -> { if (player.isOnline()) { addPlayer(player); } }, PING_INTERVAL); } @EventHandler public void onPlayerQuit(PlayerQuitEvent quitEvent) { removePlayer(quitEvent.getPlayer()); } public void clear() { playerHistory.clear(); } } ================================================ FILE: src/main/java/com/github/games647/lagmonitor/task/TPSHistoryTask.java ================================================ package com.github.games647.lagmonitor.task; import com.github.games647.lagmonitor.util.RollingOverHistory; import java.util.concurrent.TimeUnit; public class TPSHistoryTask implements Runnable { public static final int RUN_INTERVAL = 20; private static final int ONE_MINUTE = (int) TimeUnit.MINUTES.toSeconds(1); private final RollingOverHistory minuteSample = new RollingOverHistory(ONE_MINUTE, 20.0F); private final RollingOverHistory quarterSample = new RollingOverHistory(ONE_MINUTE * 15, 20.0F); private final RollingOverHistory halfHourSample = new RollingOverHistory(ONE_MINUTE * 30, 20.0F); //the last time we updated the ticks private long lastCheck = System.nanoTime(); public RollingOverHistory getMinuteSample() { return minuteSample; } public RollingOverHistory getQuarterSample() { return quarterSample; } public RollingOverHistory getHalfHourSample() { return halfHourSample; } public float getLastSample() { synchronized (this) { int lastPos = minuteSample.getCurrentPosition(); return minuteSample.getSamples()[lastPos]; } } @Override public void run() { //nanoTime is more accurate long currentTime = System.nanoTime(); long timeSpent = currentTime - lastCheck; //update the last check lastCheck = currentTime; //how many ticks passed since the last check * 1000 to convert to seconds float tps = 1 * 20 * 1000.0F / (timeSpent / (1000 * 1000)); if (tps >= 0.0F && tps < 25.0F) { //Prevent all invalid values synchronized (this) { minuteSample.add(tps); quarterSample.add(tps); halfHourSample.add(tps); } } } } ================================================ FILE: src/main/java/com/github/games647/lagmonitor/threading/BlockingActionManager.java ================================================ package com.github.games647.lagmonitor.threading; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import java.util.Map; import java.util.Set; import java.util.logging.Level; import org.bukkit.Bukkit; import org.bukkit.event.Listener; import org.bukkit.plugin.Plugin; import org.bukkit.plugin.java.JavaPlugin; public class BlockingActionManager implements Listener { //feel free to improve the wording of this: private static final String THREAD_SAFETY_NOTICE = "As threads **can** run concurrently or in parallel " + "shared data access has to be synchronized (thread-safety) in order to prevent " + "unexpected behavior or crashes. "; private static final String SAFETY_METHODS = "You can guarantee thread-safety by " + "running the data access always on the same thread, using atomic operations, " + "locks (ex: a synchronized block), immutable objects, thread local data " + "or something similar. "; private static final String COMMON_SAFE = "Common things that are thread-safe: Logging, Bukkit Scheduler, " + "Concurrent collections (ex: ConcurrentHashMap or Collections.synchronized*), ... "; private static final String BLOCKING_ACTION_MESSAGE = "Plugin {0} is performing a blocking I/O operation ({1}) " + "on the main thread. " + "This could affect the server performance, because the thread pauses until it gets the response. " + "Such operations should be performed asynchronous from the main thread. " + "Besides gameplay performance it could also improve startup time. " + "Keep in mind to keep the code thread-safe. "; private final Plugin plugin; private final Set violations = Sets.newConcurrentHashSet(); private final Set violatedPlugins = Sets.newConcurrentHashSet(); public BlockingActionManager(Plugin plugin) { this.plugin = plugin; } public void checkBlockingAction(String event) { if (!Bukkit.isPrimaryThread()) { return; } logCurrentStack(BLOCKING_ACTION_MESSAGE, event); } public void checkThreadSafety(String eventName) { if (Bukkit.isPrimaryThread()) { return; } logCurrentStack("Plugin {0} triggered an synchronous event {1} from an asynchronous Thread. " + THREAD_SAFETY_NOTICE + "Use runTask* (no Async*), scheduleSync* or callSyncMethod to run on the main thread.", eventName); } public void logCurrentStack(String format, String eventName) { IllegalAccessException stackTraceCreator = new IllegalAccessException(); StackTraceElement[] stackTrace = stackTraceCreator.getStackTrace(); Map.Entry foundPlugin = findPlugin(stackTrace); PluginViolation violation = new PluginViolation(eventName); if (foundPlugin != null) { String pluginName = foundPlugin.getKey(); violation = new PluginViolation(pluginName, foundPlugin.getValue(), eventName); if (!violatedPlugins.add(violation.getPluginName()) && plugin.getConfig().getBoolean("oncePerPlugin")) { return; } } if (!violations.add(violation)) { return; } plugin.getLogger().log(Level.WARNING, format + "Report it to the plugin author" , new Object[]{violation.getPluginName(), eventName}); if (plugin.getConfig().getBoolean("hideStacktrace")) { plugin.getLogger().log(Level.WARNING, "Source: {0}, method {1}, line {2}" , new Object[]{violation.getSourceFile(), violation.getMethodName(), violation.getLineNumber()}); } else { plugin.getLogger().log(Level.WARNING, "The following exception is not an error. " + "It's a hint for the plugin developers to find the source. " + plugin.getName() + " doesn't prevent this action. It just warns you about it. ", stackTraceCreator); } } public Map.Entry findPlugin(StackTraceElement[] stacktrace) { boolean skipping = true; for (StackTraceElement elem : stacktrace) { try { Class clazz = Class.forName(elem.getClassName()); if (clazz.getName().endsWith("VanillaCommandWrapper")) { //explicit use getName instead of SimpleName because getSimpleBinaryName causes a //StringIndexOutOfBoundsException for obfuscated plugins return Maps.immutableEntry("Vanilla", elem); } Plugin plugin; try { plugin = JavaPlugin.getProvidingPlugin(clazz); if (plugin == this.plugin) { continue; } return Maps.immutableEntry(plugin.getName(), elem); } catch (IllegalArgumentException illegalArgumentEx) { //ignore } } catch (ClassNotFoundException ex) { //if this class cannot be loaded then it could be something native so we ignore it } } return null; } } ================================================ FILE: src/main/java/com/github/games647/lagmonitor/threading/BlockingSecurityManager.java ================================================ package com.github.games647.lagmonitor.threading; import com.google.common.collect.ImmutableSet; import java.io.FilePermission; import java.security.Permission; import java.util.Set; public class BlockingSecurityManager extends SecurityManager implements Injectable { private final BlockingActionManager actionManager; private final Set allowedFiles = ImmutableSet.of(".jar", "session.lock"); private SecurityManager delegate; public BlockingSecurityManager(BlockingActionManager actionManager) { this.actionManager = actionManager; } @Override public void checkPermission(Permission perm, Object context) { if (delegate != null) { delegate.checkPermission(perm, context); } checkMainThreadOperation(perm); } @Override public void checkPermission(Permission perm) { if (delegate != null) { delegate.checkPermission(perm); } checkMainThreadOperation(perm); } private void checkMainThreadOperation(Permission perm) { if (isBlockingAction(perm)) { actionManager.checkBlockingAction("Permission: " + perm.getName()); } } private boolean isBlockingAction(Permission permission) { String actions = permission.getActions(); return permission instanceof FilePermission && actions.contains("read") && allowedFiles.stream().noneMatch(ignored -> permission.getName().contains(ignored)); } @Override public void inject() { SecurityManager oldSecurityManager = System.getSecurityManager(); if (oldSecurityManager != this) { this.delegate = oldSecurityManager; System.setSecurityManager(this); } } @Override public void restore() { if (System.getSecurityManager() == this) { System.setSecurityManager(delegate); } } } ================================================ FILE: src/main/java/com/github/games647/lagmonitor/threading/Injectable.java ================================================ package com.github.games647.lagmonitor.threading; public interface Injectable { void inject(); void restore(); } ================================================ FILE: src/main/java/com/github/games647/lagmonitor/threading/PluginViolation.java ================================================ package com.github.games647.lagmonitor.threading; import java.util.Objects; public class PluginViolation { private final String pluginName; private final String sourceFile; private final String methodName; private final int lineNumber; private final String event; public PluginViolation(String pluginName, StackTraceElement stackTraceElement, String event) { this.pluginName = pluginName; this.sourceFile = stackTraceElement.getFileName(); this.methodName = stackTraceElement.getMethodName(); this.lineNumber = stackTraceElement.getLineNumber(); this.event = event; } public PluginViolation(String event) { this.pluginName = "Unknown"; this.sourceFile = "Unknown"; this.methodName = "Unknown"; this.lineNumber = -1; this.event = event; } public String getPluginName() { return pluginName; } public String getSourceFile() { return sourceFile; } public String getMethodName() { return methodName; } public int getLineNumber() { return lineNumber; } public String getEvent() { return event; } @Override public int hashCode() { return Objects.hash(pluginName, sourceFile, methodName); } @Override public boolean equals(Object obj) { if (!(obj instanceof PluginViolation)) { return false; } PluginViolation other = (PluginViolation) obj; return Objects.equals(pluginName, other.pluginName) && Objects.equals(sourceFile, other.sourceFile) && Objects.equals(methodName, other.methodName); } @Override public String toString() { return this.getClass().getSimpleName() + '{' + "pluginName='" + pluginName + '\'' + ", sourceFile='" + sourceFile + '\'' + ", methodName='" + methodName + '\'' + ", lineNumber=" + lineNumber + ", event='" + event + '\'' + '}'; } } ================================================ FILE: src/main/java/com/github/games647/lagmonitor/traffic/CleanUpTask.java ================================================ package com.github.games647.lagmonitor.traffic; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.channel.ChannelPipeline; import java.util.NoSuchElementException; public class CleanUpTask implements Runnable { private final ChannelPipeline pipeline; private final ChannelInboundHandlerAdapter serverChannelHandler; public CleanUpTask(ChannelPipeline pipeline, ChannelInboundHandlerAdapter serverChannelHandler) { this.pipeline = pipeline; this.serverChannelHandler = serverChannelHandler; } @Override public void run() { try { pipeline.remove(serverChannelHandler); } catch (NoSuchElementException e) { // That's fine } } } ================================================ FILE: src/main/java/com/github/games647/lagmonitor/traffic/Reflection.java ================================================ package com.github.games647.lagmonitor.traffic; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.Arrays; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.bukkit.Bukkit; /** * An utility class that simplifies reflection in Bukkit plugins. * * @author Kristian */ public final class Reflection { /** * An interface for invoking a specific constructor. */ @FunctionalInterface public interface ConstructorInvoker { /** * Invoke a constructor for a specific class. * * @param arguments - the arguments to pass to the constructor. * @return The constructed object. */ Object invoke(Object... arguments); } /** * An interface for invoking a specific method. */ @FunctionalInterface public interface MethodInvoker { /** * Invoke a method on a specific target object. * * @param target - the target object, or NULL for a static method. * @param arguments - the arguments to pass to the method. * @return The return value, or NULL if is void. */ Object invoke(Object target, Object... arguments); } /** * An interface for retrieving the field content. * * @param - field type. */ public interface FieldAccessor { /** * Retrieve the content of a field. * * @param target - the target object, or NULL for a static field. * @return The value of the field. */ T get(Object target); /** * Set the content of a field. * * @param target - the target object, or NULL for a static field. * @param value - the new value of the field. */ void set(Object target, Object value); /** * Determine if the given object has this field. * * @param target - the object to test. * @return TRUE if it does, FALSE otherwise. */ boolean hasField(Object target); } // Deduce the net.minecraft.server.v* package private static final String OBC_PREFIX = Bukkit.getServer().getClass().getPackage().getName(); private static final String NMS_PREFIX = "net.minecraft.server"; private static final String NMS_PREFIX_VERSIONED = OBC_PREFIX.replace("org.bukkit.craftbukkit", NMS_PREFIX); private static final String VERSION = OBC_PREFIX.replace("org.bukkit.craftbukkit", "").replace(".", ""); // Variable replacement private static final Pattern MATCH_VARIABLE = Pattern.compile("\\{([^}]+)}"); private Reflection() { // Seal class } /** * Retrieve a field accessor for a specific field type and name. * * @param target - the target type. * @param name - the name of the field, or NULL to ignore. * @param fieldType - a compatible field type. * @return The field accessor. */ public static FieldAccessor getField(Class target, String name, Class fieldType) { return getField(target, name, fieldType, 0); } /** * Retrieve a field accessor for a specific field type and name. * * @param className - lookup name of the class, see {@link #getClass(String)}. * @param name - the name of the field, or NULL to ignore. * @param fieldType - a compatible field type. * @return The field accessor. */ public static FieldAccessor getField(String className, String name, Class fieldType) { return getField(getClass(className), name, fieldType, 0); } /** * Retrieve a field accessor for a specific field type and name. * * @param target - the target type. * @param fieldType - a compatible field type. * @param index - the number of compatible fields to skip. * @return The field accessor. */ public static FieldAccessor getField(Class target, Class fieldType, int index) { return getField(target, null, fieldType, index); } /** * Retrieve a field accessor for a specific field type and name. * * @param className - lookup name of the class, see {@link #getClass(String)}. * @param fieldType - a compatible field type. * @param index - the number of compatible fields to skip. * @return The field accessor. */ public static FieldAccessor getField(String className, Class fieldType, int index) { return getField(getClass(className), fieldType, index); } // Common method private static FieldAccessor getField(Class target, String name, Class fieldType, int index) { for (final Field field : target.getDeclaredFields()) { if ((name == null || field.getName().equals(name)) && fieldType.isAssignableFrom(field.getType()) && index-- <= 0) { field.setAccessible(true); // A function for retrieving a specific field value return new FieldAccessor() { @Override @SuppressWarnings("unchecked") public T get(Object target) { try { return (T) field.get(target); } catch (IllegalAccessException e) { throw new RuntimeException("Cannot access reflection.", e); } } @Override public void set(Object target, Object value) { try { field.set(target, value); } catch (IllegalAccessException e) { throw new RuntimeException("Cannot access reflection.", e); } } @Override public boolean hasField(Object target) { // target instanceof DeclaringClass return field.getDeclaringClass().isAssignableFrom(target.getClass()); } }; } } // Search in parent classes if (target.getSuperclass() != null) { return getField(target.getSuperclass(), name, fieldType, index); } throw new IllegalArgumentException("Cannot find field with type " + fieldType); } /** * Search for the first publicly and privately defined method of the given name and parameter count. * * @param className - lookup name of the class, see {@link #getClass(String)}. * @param methodName - the method name, or NULL to skip. * @param params - the expected parameters. * @return An object that invokes this specific method. * @throws IllegalStateException If we cannot find this method. */ public static MethodInvoker getMethod(String className, String methodName, Class... params) { return getTypedMethod(getClass(className), methodName, null, params); } /** * Search for the first publicly and privately defined method of the given name and parameter count. * * @param clazz - a class to start with. * @param methodName - the method name, or NULL to skip. * @param params - the expected parameters. * @return An object that invokes this specific method. * @throws IllegalStateException If we cannot find this method. */ public static MethodInvoker getMethod(Class clazz, String methodName, Class... params) { return getTypedMethod(clazz, methodName, null, params); } /** * Search for the first publicly and privately defined method of the given name and parameter count. * * @param clazz - a class to start with. * @param methodName - the method name, or NULL to skip. * @param returnType - the expected return type, or NULL to ignore. * @param params - the expected parameters. * @return An object that invokes this specific method. * @throws IllegalStateException If we cannot find this method. */ public static MethodInvoker getTypedMethod(Class clazz, String methodName, Class returnType, Class... params) { for (final Method method : clazz.getDeclaredMethods()) { if ((methodName == null || method.getName().equals(methodName)) && (returnType == null) || method.getReturnType().equals(returnType) && Arrays.equals(method.getParameterTypes(), params)) { method.setAccessible(true); return (target, arguments) -> { try { return method.invoke(target, arguments); } catch (Exception e) { throw new RuntimeException("Cannot invoke method " + method, e); } }; } } // Search in every superclass if (clazz.getSuperclass() != null) { return getMethod(clazz.getSuperclass(), methodName, params); } throw new IllegalStateException(String.format("Unable to find method %s (%s).", methodName, Arrays.asList(params))); } /** * Search for the first publicly and privately defined constructor of the given name and parameter count. * * @param className - lookup name of the class, see {@link #getClass(String)}. * @param params - the expected parameters. * @return An object that invokes this constructor. * @throws IllegalStateException If we cannot find this method. */ public static ConstructorInvoker getConstructor(String className, Class... params) { return getConstructor(getClass(className), params); } /** * Search for the first publicly and privately defined constructor of the given name and parameter count. * * @param clazz - a class to start with. * @param params - the expected parameters. * @return An object that invokes this constructor. * @throws IllegalStateException If we cannot find this method. */ public static ConstructorInvoker getConstructor(Class clazz, Class... params) { for (final Constructor constructor : clazz.getDeclaredConstructors()) { if (Arrays.equals(constructor.getParameterTypes(), params)) { constructor.setAccessible(true); return arguments -> { try { return constructor.newInstance(arguments); } catch (Exception e) { throw new RuntimeException("Cannot invoke constructor " + constructor, e); } }; } } throw new IllegalStateException(String.format("Unable to find constructor for %s (%s).", clazz, Arrays.asList(params))); } /** * Retrieve a class from its full name, without knowing its type on compile time. *

* This is useful when looking up fields by a NMS or OBC type. *

* * @see Object#getClass() * @param lookupName - the class name with variables. * @return The class. */ public static Class getUntypedClass(String lookupName) { @SuppressWarnings({"rawtypes", "unchecked"}) Class clazz = (Class) getClass(lookupName); return clazz; } /** * Retrieve a class from its full name. *

* Strings enclosed with curly brackets - such as {TEXT} - will be replaced according to the following table: *

* * * * * * * * * * * * * * * * * *
VariableContent
{nms}Actual package name of net.minecraft.server.VERSION
{obc}Actual package name of org.bukkit.craftbukkit.VERSION
{version}The current Minecraft package VERSION, if any.
* * @param lookupName - the class name with variables. * @return The looked up class. * @throws IllegalArgumentException If a variable or class could not be found. */ public static Class getClass(String lookupName) { return getCanonicalClass(expandVariables(lookupName)); } /** * Retrieve a class in the net.minecraft.server.VERSION.* package. * * @param name - the name of the class, excluding the package. * @throws IllegalArgumentException If the class doesn't exist. */ public static Class getMinecraftClass(String name, String alias) { try { return Class.forName(NMS_PREFIX + '.' + alias); } catch (ClassNotFoundException e) { return getCanonicalClass(NMS_PREFIX_VERSIONED + '.' + name); } } /** * Retrieve a class in the org.bukkit.craftbukkit.VERSION.* package. * * @param name - the name of the class, excluding the package. * @throws IllegalArgumentException If the class doesn't exist. */ public static Class getCraftBukkitClass(String name) { return getCanonicalClass(OBC_PREFIX + '.' + name); } /** * Retrieve a class by its canonical name. * * @param canonicalName - the canonical name. * @return The class. */ private static Class getCanonicalClass(String canonicalName) { try { return Class.forName(canonicalName); } catch (ClassNotFoundException e) { throw new IllegalArgumentException("Cannot find " + canonicalName, e); } } /** * Expand variables such as "{nms}" and "{obc}" to their corresponding packages. * * @param name - the full name of the class. * @return The expanded string. */ private static String expandVariables(String name) { StringBuffer output = new StringBuffer(); Matcher matcher = MATCH_VARIABLE.matcher(name); while (matcher.find()) { String variable = matcher.group(1); String replacement; // Expand all detected variables if ("nms".equalsIgnoreCase(variable)) { replacement = NMS_PREFIX_VERSIONED; } else if ("obc".equalsIgnoreCase(variable)) { replacement = OBC_PREFIX; } else if ("version".equalsIgnoreCase(variable)) { replacement = VERSION; } else { throw new IllegalArgumentException("Unknown variable: " + variable); } // Assume the expanded variables are all packages, and append a dot if (!replacement.isEmpty() && matcher.end() < name.length() && name.charAt(matcher.end()) != '.') { replacement += "."; } matcher.appendReplacement(output, Matcher.quoteReplacement(replacement)); } matcher.appendTail(output); return output.toString(); } } ================================================ FILE: src/main/java/com/github/games647/lagmonitor/traffic/TinyProtocol.java ================================================ package com.github.games647.lagmonitor.traffic; import com.github.games647.lagmonitor.traffic.Reflection.FieldAccessor; import io.netty.channel.Channel; import io.netty.channel.ChannelDuplexHandler; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.channel.ChannelPipeline; import io.netty.channel.ChannelPromise; import java.util.ArrayList; import java.util.Collection; import java.util.List; import org.bukkit.Bukkit; import org.bukkit.plugin.Plugin; /** * This is modified version of TinyProtocol from the ProtocolLib authors (dmulloy2 and aadnk). The not relevant things * are removed like player channel injection and the serverChannelHandler is modified so we can read the raw input of * the incoming and outgoing packets * * Original can be found here: * https://github.com/dmulloy2/ProtocolLib/blob/master/modules/TinyProtocol/src/main/java/com/comphenix/tinyprotocol/TinyProtocol.java */ public abstract class TinyProtocol { // Looking up ServerConnection private static final Class SERVER_CLASS = (Class) Reflection.getMinecraftClass("MinecraftServer", "MinecraftServer"); private static final Class CONNECTION_CLASS = (Class) Reflection.getMinecraftClass("ServerConnection", "network.ServerConnection"); private static final Reflection.MethodInvoker GET_SERVER = Reflection.getMethod("{obc}.CraftServer", "getServer"); private static final FieldAccessor GET_CONNECTION = Reflection.getField(SERVER_CLASS, CONNECTION_CLASS, 0); // Injected channel handlers private final Collection serverChannels = new ArrayList<>(); private ChannelInboundHandlerAdapter serverChannelHandler; private volatile boolean closed; protected final Plugin plugin; /** * Construct a new instance of TinyProtocol, and start intercepting packets for all connected clients and future * clients. *

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