diff options
3 files changed, 151 insertions, 18 deletions
diff --git a/spark-common/src/main/java/me/lucko/spark/common/SparkPlatform.java b/spark-common/src/main/java/me/lucko/spark/common/SparkPlatform.java index 50ef7d8..4d6557b 100644 --- a/spark-common/src/main/java/me/lucko/spark/common/SparkPlatform.java +++ b/spark-common/src/main/java/me/lucko/spark/common/SparkPlatform.java @@ -21,6 +21,7 @@ package me.lucko.spark.common; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import me.lucko.spark.common.activitylog.ActivityLog; import me.lucko.spark.common.command.Arguments; import me.lucko.spark.common.command.Command; @@ -36,6 +37,7 @@ import me.lucko.spark.common.command.sender.CommandSender; import me.lucko.spark.common.command.tabcomplete.CompletionSupplier; import me.lucko.spark.common.command.tabcomplete.TabCompleter; import me.lucko.spark.common.monitor.cpu.CpuMonitor; +import me.lucko.spark.common.monitor.memory.GarbageCollectorStatistics; import me.lucko.spark.common.monitor.tick.TickStatistics; import me.lucko.spark.common.sampler.tick.TickHook; import me.lucko.spark.common.sampler.tick.TickReporter; @@ -50,6 +52,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; /** @@ -71,6 +74,8 @@ public class SparkPlatform { private final TickHook tickHook; private final TickReporter tickReporter; private final TickStatistics tickStatistics; + private Map<String, GarbageCollectorStatistics> startupGcStatistics = ImmutableMap.of(); + private long serverNormalOperationStartTime; public SparkPlatform(SparkPlugin plugin) { this.plugin = plugin; @@ -108,6 +113,12 @@ public class SparkPlatform { this.tickReporter.start(); } CpuMonitor.ensureMonitoring(); + + // poll startup GC statistics after plugins & the world have loaded + this.plugin.executeAsync(() -> { + this.startupGcStatistics = GarbageCollectorStatistics.pollStats(); + this.serverNormalOperationStartTime = System.currentTimeMillis(); + }); } public void disable() { @@ -143,6 +154,14 @@ public class SparkPlatform { return this.tickStatistics; } + public Map<String, GarbageCollectorStatistics> getStartupGcStatistics() { + return this.startupGcStatistics; + } + + public long getServerNormalOperationStartTime() { + return this.serverNormalOperationStartTime; + } + public void executeCommand(CommandSender sender, String[] args) { CommandResponseHandler resp = new CommandResponseHandler(this, sender); diff --git a/spark-common/src/main/java/me/lucko/spark/common/command/modules/GcMonitoringModule.java b/spark-common/src/main/java/me/lucko/spark/common/command/modules/GcMonitoringModule.java index 2e67a99..6330797 100644 --- a/spark-common/src/main/java/me/lucko/spark/common/command/modules/GcMonitoringModule.java +++ b/spark-common/src/main/java/me/lucko/spark/common/command/modules/GcMonitoringModule.java @@ -26,14 +26,13 @@ import me.lucko.spark.common.command.Command; import me.lucko.spark.common.command.CommandModule; import me.lucko.spark.common.command.CommandResponseHandler; import me.lucko.spark.common.monitor.memory.GarbageCollectionMonitor; +import me.lucko.spark.common.monitor.memory.GarbageCollectorStatistics; import me.lucko.spark.common.util.FormatUtil; import net.kyori.text.Component; import net.kyori.text.TextComponent; import net.kyori.text.format.TextColor; import net.kyori.text.format.TextDecoration; -import java.lang.management.GarbageCollectorMXBean; -import java.lang.management.ManagementFactory; import java.lang.management.MemoryUsage; import java.text.DecimalFormat; import java.util.LinkedList; @@ -60,26 +59,30 @@ public class GcMonitoringModule implements CommandModule { consumer.accept(Command.builder() .aliases("gc") .executor((platform, sender, resp, arguments) -> { - resp.replyPrefixed(TextComponent.of("Calculating GC collection averages...")); + resp.replyPrefixed(TextComponent.of("Calculating GC statistics...")); List<Component> report = new LinkedList<>(); report.add(TextComponent.empty()); report.add(TextComponent.builder("") .append(TextComponent.builder(">").color(TextColor.DARK_GRAY).decoration(TextDecoration.BOLD, true).build()) .append(TextComponent.space()) - .append(TextComponent.of("GC collection averages", TextColor.GOLD)) + .append(TextComponent.of("Garbage Collector statistics", TextColor.GOLD)) .build() ); - for (GarbageCollectorMXBean collector : ManagementFactory.getGarbageCollectorMXBeans()) { - double collectionTime = collector.getCollectionTime(); - long collectionCount = collector.getCollectionCount(); + long serverUptime = System.currentTimeMillis() - platform.getServerNormalOperationStartTime(); + Map<String, GarbageCollectorStatistics> collectorStats = GarbageCollectorStatistics.pollStatsSubtractInitial(platform.getStartupGcStatistics()); + + for (Map.Entry<String, GarbageCollectorStatistics> collector : collectorStats.entrySet()) { + String collectorName = collector.getKey(); + double collectionTime = collector.getValue().getCollectionTime(); + long collectionCount = collector.getValue().getCollectionCount(); report.add(TextComponent.empty()); if (collectionCount == 0) { report.add(TextComponent.builder(" ") - .append(TextComponent.of(collector.getName() + " collector:", TextColor.GRAY)) + .append(TextComponent.of(collectorName + " collector:", TextColor.GRAY)) .build() ); report.add(TextComponent.builder(" ") @@ -91,9 +94,10 @@ public class GcMonitoringModule implements CommandModule { } double averageCollectionTime = collectionTime / collectionCount; + double averageFrequency = (serverUptime - collectionTime) / collectionCount; report.add(TextComponent.builder(" ") - .append(TextComponent.of(collector.getName() + " collector:", TextColor.GRAY)) + .append(TextComponent.of(collectorName + " collector:", TextColor.GRAY)) .build() ); report.add(TextComponent.builder(" ") @@ -104,6 +108,11 @@ public class GcMonitoringModule implements CommandModule { .append(TextComponent.of(" total collections", TextColor.GRAY)) .build() ); + report.add(TextComponent.builder(" ") + .append(TextComponent.of(formatTime((long) averageFrequency), TextColor.WHITE)) + .append(TextComponent.of(" avg frequency", TextColor.GRAY)) + .build() + ); } if (report.size() == 1) { @@ -130,6 +139,30 @@ public class GcMonitoringModule implements CommandModule { ); } + private static String formatTime(long millis) { + if (millis <= 0) { + return "0ms"; + } + + long second = millis / 1000; + millis = millis % 1000; + long minute = second / 60; + second = second % 60; + + StringBuilder sb = new StringBuilder(); + if (minute != 0) { + sb.append(minute).append("m "); + } + if (second != 0) { + sb.append(second).append("s "); + } + if (millis != 0) { + sb.append(millis).append("ms"); + } + + return sb.toString().trim(); + } + private static class ReportingGcMonitor extends GarbageCollectionMonitor implements GarbageCollectionMonitor.Listener { private final SparkPlatform platform; private final CommandResponseHandler resp; @@ -193,13 +226,13 @@ public class GcMonitoringModule implements CommandModule { .build() ); report.add(TextComponent.builder(" ") - .append(TextComponent.of(FormatUtil.formatBytes(before.getUsed()), TextColor.WHITE)) - .append(TextComponent.of(" → ", TextColor.GRAY)) - .append(TextComponent.of(FormatUtil.formatBytes(after.getUsed()), TextColor.WHITE)) + .append(TextComponent.of(FormatUtil.formatBytes(before.getUsed()), TextColor.GRAY)) + .append(TextComponent.of(" → ", TextColor.DARK_GRAY)) + .append(TextComponent.of(FormatUtil.formatBytes(after.getUsed()), TextColor.GRAY)) .append(TextComponent.space()) - .append(TextComponent.of("(", TextColor.GRAY)) - .append(TextComponent.of(FormatUtil.percent(diff, before.getUsed()), TextColor.GREEN)) - .append(TextComponent.of(")", TextColor.GRAY)) + .append(TextComponent.of("(", TextColor.DARK_GRAY)) + .append(TextComponent.of(FormatUtil.percent(diff, before.getUsed()), TextColor.WHITE)) + .append(TextComponent.of(")", TextColor.DARK_GRAY)) .build() ); } else { @@ -210,9 +243,9 @@ public class GcMonitoringModule implements CommandModule { .build() ); report.add(TextComponent.builder(" ") - .append(TextComponent.of(FormatUtil.formatBytes(before.getUsed()), TextColor.WHITE)) - .append(TextComponent.of(" → ", TextColor.GRAY)) - .append(TextComponent.of(FormatUtil.formatBytes(after.getUsed()), TextColor.WHITE)) + .append(TextComponent.of(FormatUtil.formatBytes(before.getUsed()), TextColor.GRAY)) + .append(TextComponent.of(" → ", TextColor.DARK_GRAY)) + .append(TextComponent.of(FormatUtil.formatBytes(after.getUsed()), TextColor.GRAY)) .build() ); } diff --git a/spark-common/src/main/java/me/lucko/spark/common/monitor/memory/GarbageCollectorStatistics.java b/spark-common/src/main/java/me/lucko/spark/common/monitor/memory/GarbageCollectorStatistics.java new file mode 100644 index 0000000..e875fd7 --- /dev/null +++ b/spark-common/src/main/java/me/lucko/spark/common/monitor/memory/GarbageCollectorStatistics.java @@ -0,0 +1,81 @@ +/* + * This file is part of spark. + * + * Copyright (c) lucko (Luck) <luck@lucko.me> + * Copyright (c) contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +package me.lucko.spark.common.monitor.memory; + +import com.google.common.collect.ImmutableMap; + +import java.lang.management.GarbageCollectorMXBean; +import java.lang.management.ManagementFactory; +import java.util.Map; + +/** + * Holder for {@link GarbageCollectorMXBean} statistics. + */ +public class GarbageCollectorStatistics { + public static final GarbageCollectorStatistics ZERO = new GarbageCollectorStatistics(0, 0); + + public static Map<String, GarbageCollectorStatistics> pollStats() { + ImmutableMap.Builder<String, GarbageCollectorStatistics> stats = ImmutableMap.builder(); + for (GarbageCollectorMXBean bean : ManagementFactory.getGarbageCollectorMXBeans()) { + stats.put(bean.getName(), new GarbageCollectorStatistics(bean)); + } + return stats.build(); + } + + public static Map<String, GarbageCollectorStatistics> pollStatsSubtractInitial(Map<String, GarbageCollectorStatistics> initial) { + ImmutableMap.Builder<String, GarbageCollectorStatistics> stats = ImmutableMap.builder(); + for (GarbageCollectorMXBean bean : ManagementFactory.getGarbageCollectorMXBeans()) { + stats.put(bean.getName(), new GarbageCollectorStatistics(bean).subtract(initial.getOrDefault(bean.getName(), ZERO))); + } + return stats.build(); + } + + private final long collectionCount; + private final long collectionTime; + + public GarbageCollectorStatistics(long collectionCount, long collectionTime) { + this.collectionCount = collectionCount; + this.collectionTime = collectionTime; + } + + public GarbageCollectorStatistics(GarbageCollectorMXBean bean) { + this(bean.getCollectionCount(), bean.getCollectionTime()); + } + + public long getCollectionCount() { + return this.collectionCount; + } + + public long getCollectionTime() { + return this.collectionTime; + } + + public GarbageCollectorStatistics subtract(GarbageCollectorStatistics other) { + if (other == ZERO || (other.collectionCount == 0 && other.collectionTime == 0)) { + return this; + } + + return new GarbageCollectorStatistics( + this.collectionCount - other.collectionCount, + this.collectionTime - other.collectionTime + ); + } +} |