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<SystemInfo> 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<String, String> 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));
    }
} public class GraphCommand extends LagCommand implements TabExecutor {

    private static final int MAX_COMBINED = 4;

    private final Map<String, GraphRenderer> 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<String> 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<GraphRenderer> 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;
    }
} 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<String, Map<String, Object>> entry : plugin.getDescription().getCommands().entrySet()) {
            String commandKey = entry.getKey();
            Map<String, Object> value = entry.getValue();

            String description = ' ' + value.getOrDefault("description", "No description").toString();
            String usage = ((String) value.getOrDefault("usage", '/' + commandKey)).replace("<command>", 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;
    }
} 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<ObjectInstance> 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<String> onTabComplete(CommandSender sender, Command command, String alias, String[] args) {
        String lastArg = args[args.length - 1];

        MBeanServer mBeanServer = ManagementFactory.getPlatformMBeanServer();

        Stream<String> result = Stream.empty();
        if (args.length == 1) {
            Set<ObjectName> 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());
    }
} 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<BaseComponent[]> 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<BaseComponent[]> 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<MethodMeasurement> childInvokes = current.getChildInvokes().values();
        List<MethodMeasurement> 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());
            }
        });
    }
} 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<SystemInfo> 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<PhysicalMemory> 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<HWDiskStore> 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<OSFileStore> 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);
    }
} 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<SystemInfo> 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);
    }
} 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);
    }
} 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<Thread, StackTraceElement[]> allStackTraces = Thread.getAllStackTraces();
            for (Map.Entry<Thread, StackTraceElement[]> 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<BaseComponent[]> 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<String> onTabComplete(CommandSender sender, Command command, String alias, String[] args) {
        List<String> 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());
    }
} 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();
    }
} 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);
    }
} 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<BaseComponent[]> 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");
        }
    }
} 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<BaseComponent[]> lines = new ArrayList<>();

        Map<Thread, StackTraceElement[]> 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");
        }
    }
} 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;
    }
} 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<OSProcess> 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<World> 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();
    }
} 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<StringBuilder> 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<StringBuilder> 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<StringBuilder> 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);
        }
    }
} 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<BaseComponent[]> lines = new ArrayList<>();

        List<BukkitTask> 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;
    }
} 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<TimingHistory> 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<BaseComponent[]> 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<BaseComponent[]> lines, TimingHistory lastHistory) {
        printHeadData(lastHistory, lines);

        Map<Integer, String> 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<Integer, String> idMap,
                               Collection<BaseComponent[]> 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<BaseComponent[]> 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)));
    }
} 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<CustomTimingsHandler> handlers = Reflection.getField(CustomTimingsHandler.class, "HANDLERS", Queue.class)
                .get(null);

        List<BaseComponent[]> 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<CustomTimingsHandler> handlers, Collection<BaseComponent[]> lines,
                                  long sampleTime) {
        Map<String, Timing> 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<String, Timing> 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<String, Timing> 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<String, Timing> 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<BaseComponent[]> 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<CustomTimingsHandler> handlers, Map<String, Timing> timings
            , Timing minecraftTiming, Timing breakdownTiming) {
//        FieldAccessor<CustomTimingsHandler> getParent = Reflection
//                .getField(CustomTimingsHandler.class, "parent", CustomTimingsHandler.class);
        FieldAccessor<String> getName = Reflection.getField(CustomTimingsHandler.class, "name", String.class);

        FieldAccessor<Long> getTotalTime = Reflection.getField(CustomTimingsHandler.class, "totalTime", Long.TYPE);
        FieldAccessor<Long> getCount = Reflection.getField(CustomTimingsHandler.class, "count", Long.TYPE);
//        FieldAccessor<Long> 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);
    }
} 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<Proxy> 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);
        }
    }
} 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);
    }
} public class PageManager implements Listener {

    private final Map<String, Pages> 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);
    }
} 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);
        }
    }
} 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<UUID, WorldData> worldsData = getWorldData();
            if (!storage.saveWorlds(monitorId, worldsData.values())) {
                //error occurred
                return;
            }

            List<PlayerData> playerData = getPlayerData(worldsData);
            storage.savePlayers(playerData);
        } catch (ExecutionException | InterruptedException ex) {
            plugin.getLogger().log(Level.SEVERE, "Error saving monitoring data", ex);
        }
    }

    private List<PlayerData> getPlayerData(final Map<UUID, WorldData> worldsData)
            throws InterruptedException, ExecutionException {
        Future<List<PlayerData>> playerFuture = Bukkit.getScheduler()
                .callSyncMethod(plugin, () -> {
                    Collection<? extends Player> onlinePlayers = Bukkit.getOnlinePlayers();
                    List<PlayerData> 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<UUID, WorldData> getWorldData()
            throws ExecutionException, InterruptedException {
        //this is not thread-safe and have to run sync

        Future<Map<UUID, WorldData>> worldFuture = Bukkit.getScheduler()
                .callSyncMethod(plugin, () -> {
                            List<World> worlds = Bukkit.getWorlds();
                            Map<UUID, WorldData> worldsData = Maps.newHashMapWithExpectedSize(worlds.size());
                            for (World world : worlds) {
                                worldsData.put(world.getUID(), WorldData.fromWorld(world));
                            }

                            return worldsData;
                        });

        Map<UUID, WorldData> 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);
    }
} 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> systemInfo = plugin.getNativeData().getSystemInfo();
        if (systemInfo.isPresent()) {
            List<NetworkIF> 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<OSProcess> 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);
    }
} 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<WorldData> 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> 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});
        }
    }
} 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);
        }
    }
} 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);
    }
} 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<String, RollingOverHistory> 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<Class<? extends PingFetcher>> fetchurs = Lists.newArrayList(
                SpigotPing.class, PaperPing.class, ReflectionPing.class
        );
        for (Class<? extends PingFetcher> 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();
    }
} 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);
            }
        }
    }
} 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<PluginViolation> violations = Sets.newConcurrentHashSet();
    private final Set<String> 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. ", eventName);
        plugin.getLogger().info(THREAD_SAFETY_NOTICE);
        plugin.getLogger().info("Use runTask* (no Async*), scheduleSync* or callSyncMethod to run on the main thread.");
    }

    public void logCurrentStack(String format, String eventName) {
        IllegalAccessException stackTraceCreator = new IllegalAccessException();
        StackTraceElement[] stackTrace = stackTraceCreator.getStackTrace();

        Map.Entry<String, StackTraceElement> 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<String, StackTraceElement> 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;
    }
} public class BlockingSecurityManager extends SecurityManager implements Injectable {

    private final BlockingActionManager actionManager;
    private final Set<String> 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);
        }
    }
} public interface Injectable {

    void inject();

    void restore();
} 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);
            }
        }
    }
}
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.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;
package com.github.games647.lagmonitor;




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() {
        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")) {
            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 from the main thread
            scheduler.runTaskTimerAsynchronously(this, new MonitorSaveTask(this, storage),