diff options
Diffstat (limited to 'spark-common')
44 files changed, 1171 insertions, 1146 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 0cc2144..a5dadba 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 @@ -22,6 +22,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; @@ -39,10 +40,12 @@ 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; +import me.lucko.spark.common.tick.TickHook; +import me.lucko.spark.common.tick.TickReporter; import me.lucko.spark.common.util.BytebinClient; + import net.kyori.adventure.text.event.ClickEvent; + import okhttp3.OkHttpClient; import java.util.ArrayList; @@ -52,9 +55,15 @@ import java.util.List; import java.util.Map; import java.util.stream.Collectors; -import static net.kyori.adventure.text.Component.*; -import static net.kyori.adventure.text.format.NamedTextColor.*; -import static net.kyori.adventure.text.format.TextDecoration.*; +import static net.kyori.adventure.text.Component.space; +import static net.kyori.adventure.text.Component.text; +import static net.kyori.adventure.text.format.NamedTextColor.DARK_GRAY; +import static net.kyori.adventure.text.format.NamedTextColor.GOLD; +import static net.kyori.adventure.text.format.NamedTextColor.GRAY; +import static net.kyori.adventure.text.format.NamedTextColor.RED; +import static net.kyori.adventure.text.format.NamedTextColor.WHITE; +import static net.kyori.adventure.text.format.TextDecoration.BOLD; +import static net.kyori.adventure.text.format.TextDecoration.UNDERLINED; /** * Abstract spark implementation used by all platforms. diff --git a/spark-common/src/main/java/me/lucko/spark/common/SparkPlugin.java b/spark-common/src/main/java/me/lucko/spark/common/SparkPlugin.java index 8d18b54..171367e 100644 --- a/spark-common/src/main/java/me/lucko/spark/common/SparkPlugin.java +++ b/spark-common/src/main/java/me/lucko/spark/common/SparkPlugin.java @@ -23,8 +23,8 @@ package me.lucko.spark.common; import me.lucko.spark.common.command.sender.CommandSender; import me.lucko.spark.common.platform.PlatformInfo; import me.lucko.spark.common.sampler.ThreadDumper; -import me.lucko.spark.common.sampler.tick.TickHook; -import me.lucko.spark.common.sampler.tick.TickReporter; +import me.lucko.spark.common.tick.TickHook; +import me.lucko.spark.common.tick.TickReporter; import java.nio.file.Path; import java.util.stream.Stream; diff --git a/spark-common/src/main/java/me/lucko/spark/common/activitylog/Activity.java b/spark-common/src/main/java/me/lucko/spark/common/activitylog/Activity.java new file mode 100644 index 0000000..561515a --- /dev/null +++ b/spark-common/src/main/java/me/lucko/spark/common/activitylog/Activity.java @@ -0,0 +1,111 @@ +/* + * 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.activitylog; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; + +import me.lucko.spark.common.command.sender.CommandSender; + +import java.util.concurrent.TimeUnit; + +public final class Activity { + private final CommandSender.Data user; + private final long time; + private final String type; + + private final String dataType; + private final String dataValue; + + public static Activity urlActivity(CommandSender user, long time, String type, String url) { + return new Activity(user.toData(), time, type, "url", url); + } + + public static Activity fileActivity(CommandSender user, long time, String type, String filePath) { + return new Activity(user.toData(), time, type, "file", filePath); + } + + private Activity(CommandSender.Data user, long time, String type, String dataType, String dataValue) { + this.user = user; + this.time = time; + this.type = type; + this.dataType = dataType; + this.dataValue = dataValue; + } + + public CommandSender.Data getUser() { + return this.user; + } + + public long getTime() { + return this.time; + } + + public String getType() { + return this.type; + } + + public String getDataType() { + return this.dataType; + } + + public String getDataValue() { + return this.dataValue; + } + + public boolean shouldExpire() { + if (this.dataType.equals("url")) { + return (System.currentTimeMillis() - this.time) > TimeUnit.DAYS.toMillis(60); + } else { + return false; + } + } + + public JsonObject serialize() { + JsonObject object = new JsonObject(); + + object.add("user", this.user.serialize()); + object.add("time", new JsonPrimitive(this.time)); + object.add("type", new JsonPrimitive(this.type)); + + JsonObject data = new JsonObject(); + data.add("type", new JsonPrimitive(this.dataType)); + data.add("value", new JsonPrimitive(this.dataValue)); + object.add("data", data); + + return object; + } + + public static Activity deserialize(JsonElement element) { + JsonObject object = element.getAsJsonObject(); + + CommandSender.Data user = CommandSender.Data.deserialize(object.get("user")); + long time = object.get("time").getAsJsonPrimitive().getAsLong(); + String type = object.get("type").getAsJsonPrimitive().getAsString(); + + JsonObject dataObject = object.get("data").getAsJsonObject(); + String dataType = dataObject.get("type").getAsJsonPrimitive().getAsString(); + String dataValue = dataObject.get("value").getAsJsonPrimitive().getAsString(); + + return new Activity(user, time, type, dataType, dataValue); + } +} diff --git a/spark-common/src/main/java/me/lucko/spark/common/activitylog/ActivityLog.java b/spark-common/src/main/java/me/lucko/spark/common/activitylog/ActivityLog.java index b344ded..2693962 100644 --- a/spark-common/src/main/java/me/lucko/spark/common/activitylog/ActivityLog.java +++ b/spark-common/src/main/java/me/lucko/spark/common/activitylog/ActivityLog.java @@ -24,10 +24,7 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonArray; import com.google.gson.JsonElement; -import com.google.gson.JsonObject; import com.google.gson.JsonParser; -import com.google.gson.JsonPrimitive; -import me.lucko.spark.common.command.sender.CommandSender; import java.io.BufferedReader; import java.io.BufferedWriter; @@ -37,7 +34,6 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.LinkedList; import java.util.List; -import java.util.concurrent.TimeUnit; public class ActivityLog { @@ -132,86 +128,4 @@ public class ActivityLog { } } - public static final class Activity { - private final CommandSender.Data user; - private final long time; - private final String type; - - private final String dataType; - private final String dataValue; - - public static Activity urlActivity(CommandSender user, long time, String type, String url) { - return new Activity(user.toData(), time, type, "url", url); - } - - public static Activity fileActivity(CommandSender user, long time, String type, String filePath) { - return new Activity(user.toData(), time, type, "file", filePath); - } - - private Activity(CommandSender.Data user, long time, String type, String dataType, String dataValue) { - this.user = user; - this.time = time; - this.type = type; - this.dataType = dataType; - this.dataValue = dataValue; - } - - public CommandSender.Data getUser() { - return this.user; - } - - public long getTime() { - return this.time; - } - - public String getType() { - return this.type; - } - - public String getDataType() { - return this.dataType; - } - - public String getDataValue() { - return this.dataValue; - } - - public boolean shouldExpire() { - if (this.dataType.equals("url")) { - return (System.currentTimeMillis() - this.time) > TimeUnit.DAYS.toMillis(60); - } else { - return false; - } - } - - public JsonObject serialize() { - JsonObject object = new JsonObject(); - - object.add("user", this.user.serialize()); - object.add("time", new JsonPrimitive(this.time)); - object.add("type", new JsonPrimitive(this.type)); - - JsonObject data = new JsonObject(); - data.add("type", new JsonPrimitive(this.dataType)); - data.add("value", new JsonPrimitive(this.dataValue)); - object.add("data", data); - - return object; - } - - public static Activity deserialize(JsonElement element) { - JsonObject object = element.getAsJsonObject(); - - CommandSender.Data user = CommandSender.Data.deserialize(object.get("user")); - long time = object.get("time").getAsJsonPrimitive().getAsLong(); - String type = object.get("type").getAsJsonPrimitive().getAsString(); - - JsonObject dataObject = object.get("data").getAsJsonObject(); - String dataType = dataObject.get("type").getAsJsonPrimitive().getAsString(); - String dataValue = dataObject.get("value").getAsJsonPrimitive().getAsString(); - - return new Activity(user, time, type, dataType, dataValue); - } - } - } diff --git a/spark-common/src/main/java/me/lucko/spark/common/command/Arguments.java b/spark-common/src/main/java/me/lucko/spark/common/command/Arguments.java index 3cd0365..17c49e2 100644 --- a/spark-common/src/main/java/me/lucko/spark/common/command/Arguments.java +++ b/spark-common/src/main/java/me/lucko/spark/common/command/Arguments.java @@ -30,6 +30,9 @@ import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; +/** + * Utility for parsing command-flag like arguments from raw space split strings. + */ public class Arguments { private static final Pattern FLAG_REGEX = Pattern.compile("^--(.+)$"); @@ -110,19 +113,8 @@ public class Arguments { } public static final class ParseException extends IllegalArgumentException { - public ParseException() { - } - public ParseException(String s) { super(s); } - - public ParseException(String message, Throwable cause) { - super(message, cause); - } - - public ParseException(Throwable cause) { - super(cause); - } } } diff --git a/spark-common/src/main/java/me/lucko/spark/common/command/Command.java b/spark-common/src/main/java/me/lucko/spark/common/command/Command.java index e1a5146..dad15e6 100644 --- a/spark-common/src/main/java/me/lucko/spark/common/command/Command.java +++ b/spark-common/src/main/java/me/lucko/spark/common/command/Command.java @@ -21,6 +21,7 @@ package me.lucko.spark.common.command; import com.google.common.collect.ImmutableList; + import me.lucko.spark.common.SparkPlatform; import me.lucko.spark.common.command.sender.CommandSender; diff --git a/spark-common/src/main/java/me/lucko/spark/common/command/CommandResponseHandler.java b/spark-common/src/main/java/me/lucko/spark/common/command/CommandResponseHandler.java index 874939e..1acb3dc 100644 --- a/spark-common/src/main/java/me/lucko/spark/common/command/CommandResponseHandler.java +++ b/spark-common/src/main/java/me/lucko/spark/common/command/CommandResponseHandler.java @@ -22,6 +22,7 @@ package me.lucko.spark.common.command; import me.lucko.spark.common.SparkPlatform; import me.lucko.spark.common.command.sender.CommandSender; + import net.kyori.adventure.text.Component; import net.kyori.adventure.text.TextComponent; @@ -29,9 +30,11 @@ import java.util.Set; import java.util.function.Consumer; import java.util.stream.Collectors; -import static net.kyori.adventure.text.Component.*; -import static net.kyori.adventure.text.format.NamedTextColor.*; -import static net.kyori.adventure.text.format.TextDecoration.*; +import static net.kyori.adventure.text.Component.text; +import static net.kyori.adventure.text.format.NamedTextColor.DARK_GRAY; +import static net.kyori.adventure.text.format.NamedTextColor.GRAY; +import static net.kyori.adventure.text.format.NamedTextColor.YELLOW; +import static net.kyori.adventure.text.format.TextDecoration.BOLD; public class CommandResponseHandler { @@ -94,5 +97,4 @@ public class CommandResponseHandler { return PREFIX.append(message); } - } diff --git a/spark-common/src/main/java/me/lucko/spark/common/command/modules/ActivityLogModule.java b/spark-common/src/main/java/me/lucko/spark/common/command/modules/ActivityLogModule.java index ae4613a..2bdb5d6 100644 --- a/spark-common/src/main/java/me/lucko/spark/common/command/modules/ActivityLogModule.java +++ b/spark-common/src/main/java/me/lucko/spark/common/command/modules/ActivityLogModule.java @@ -20,10 +20,11 @@ package me.lucko.spark.common.command.modules; -import me.lucko.spark.common.activitylog.ActivityLog.Activity; +import me.lucko.spark.common.activitylog.Activity; import me.lucko.spark.common.command.Command; import me.lucko.spark.common.command.CommandModule; import me.lucko.spark.common.command.tabcomplete.TabCompleter; + import net.kyori.adventure.text.Component; import net.kyori.adventure.text.TextComponent; import net.kyori.adventure.text.event.ClickEvent; @@ -36,10 +37,15 @@ import java.util.Collection; import java.util.List; import java.util.function.Consumer; -import static me.lucko.spark.common.command.CommandResponseHandler.*; -import static net.kyori.adventure.text.Component.*; -import static net.kyori.adventure.text.format.NamedTextColor.*; -import static net.kyori.adventure.text.format.TextDecoration.*; +import static me.lucko.spark.common.command.CommandResponseHandler.applyPrefix; +import static net.kyori.adventure.text.Component.space; +import static net.kyori.adventure.text.Component.text; +import static net.kyori.adventure.text.format.NamedTextColor.DARK_GRAY; +import static net.kyori.adventure.text.format.NamedTextColor.GOLD; +import static net.kyori.adventure.text.format.NamedTextColor.GRAY; +import static net.kyori.adventure.text.format.NamedTextColor.WHITE; +import static net.kyori.adventure.text.format.NamedTextColor.YELLOW; +import static net.kyori.adventure.text.format.TextDecoration.BOLD; public class ActivityLogModule implements CommandModule, RowRenderer<Activity> { 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 d66a181..8e2d199 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 @@ -21,6 +21,7 @@ package me.lucko.spark.common.command.modules; import com.sun.management.GarbageCollectionNotificationInfo; + import me.lucko.spark.common.SparkPlatform; import me.lucko.spark.common.command.Command; import me.lucko.spark.common.command.CommandModule; @@ -28,6 +29,7 @@ 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.adventure.text.Component; import java.lang.management.MemoryUsage; @@ -37,12 +39,18 @@ import java.util.List; import java.util.Map; import java.util.function.Consumer; -import static net.kyori.adventure.text.Component.*; -import static net.kyori.adventure.text.format.NamedTextColor.*; -import static net.kyori.adventure.text.format.TextDecoration.*; +import static net.kyori.adventure.text.Component.empty; +import static net.kyori.adventure.text.Component.space; +import static net.kyori.adventure.text.Component.text; +import static net.kyori.adventure.text.format.NamedTextColor.DARK_GRAY; +import static net.kyori.adventure.text.format.NamedTextColor.GOLD; +import static net.kyori.adventure.text.format.NamedTextColor.GRAY; +import static net.kyori.adventure.text.format.NamedTextColor.RED; +import static net.kyori.adventure.text.format.NamedTextColor.WHITE; +import static net.kyori.adventure.text.format.TextDecoration.BOLD; public class GcMonitoringModule implements CommandModule { - private static final DecimalFormat df = new DecimalFormat("#.##"); + private static final DecimalFormat DF = new DecimalFormat("#.##"); /** The gc monitoring instance currently running, if any */ private ReportingGcMonitor activeGcMonitor = null; @@ -106,7 +114,7 @@ public class GcMonitoringModule implements CommandModule { ); report.add(text() .content(" ") - .append(text(df.format(averageCollectionTime), GOLD)) + .append(text(DF.format(averageCollectionTime), GOLD)) .append(text(" ms avg", GRAY)) .append(text(", ", DARK_GRAY)) .append(text(collectionCount, WHITE)) @@ -147,11 +155,10 @@ public class GcMonitoringModule implements CommandModule { private static String formatTime(long millis) { if (millis <= 0) { - return "0ms"; + return "0s"; } long second = millis / 1000; - //millis = millis % 1000; long minute = second / 60; second = second % 60; @@ -162,9 +169,6 @@ public class GcMonitoringModule implements CommandModule { if (second != 0) { sb.append(second).append("s "); } - //if (millis != 0) { - // sb.append(millis).append("ms"); - //} return sb.toString().trim(); } @@ -185,15 +189,7 @@ public class GcMonitoringModule implements CommandModule { @Override public void onGc(GarbageCollectionNotificationInfo data) { - String gcType; - if (data.getGcAction().equals("end of minor GC")) { - gcType = "Young Gen"; - } else if (data.getGcAction().equals("end of major GC")) { - gcType = "Old Gen"; - } else { - gcType = data.getGcAction(); - } - + String gcType = GarbageCollectionMonitor.getGcType(data); String gcCause = data.getGcCause() != null ? " (cause = " + data.getGcCause() + ")" : ""; Map<String, MemoryUsage> beforeUsages = data.getGcInfo().getMemoryUsageBeforeGc(); @@ -207,7 +203,7 @@ public class GcMonitoringModule implements CommandModule { .append(text(gcType + " ")) .append(text("GC", RED)) .append(text(" lasting ")) - .append(text(df.format(data.getGcInfo().getDuration()), GOLD)) + .append(text(DF.format(data.getGcInfo().getDuration()), GOLD)) .append(text(" ms." + gcCause)) .build() )); diff --git a/spark-common/src/main/java/me/lucko/spark/common/command/modules/HealthModule.java b/spark-common/src/main/java/me/lucko/spark/common/command/modules/HealthModule.java index 409eb38..c8f25c7 100644 --- a/spark-common/src/main/java/me/lucko/spark/common/command/modules/HealthModule.java +++ b/spark-common/src/main/java/me/lucko/spark/common/command/modules/HealthModule.java @@ -21,13 +21,19 @@ package me.lucko.spark.common.command.modules; import com.google.common.base.Strings; + +import me.lucko.spark.common.SparkPlatform; +import me.lucko.spark.common.command.Arguments; 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.command.sender.CommandSender; import me.lucko.spark.common.command.tabcomplete.TabCompleter; import me.lucko.spark.common.monitor.cpu.CpuMonitor; import me.lucko.spark.common.monitor.tick.TickStatistics; import me.lucko.spark.common.util.FormatUtil; import me.lucko.spark.common.util.RollingAverage; + import net.kyori.adventure.text.Component; import net.kyori.adventure.text.TextComponent; import net.kyori.adventure.text.format.TextColor; @@ -45,9 +51,17 @@ import java.util.LinkedList; import java.util.List; import java.util.function.Consumer; -import static net.kyori.adventure.text.Component.*; -import static net.kyori.adventure.text.format.NamedTextColor.*; -import static net.kyori.adventure.text.format.TextDecoration.*; +import static net.kyori.adventure.text.Component.empty; +import static net.kyori.adventure.text.Component.space; +import static net.kyori.adventure.text.Component.text; +import static net.kyori.adventure.text.format.NamedTextColor.DARK_GRAY; +import static net.kyori.adventure.text.format.NamedTextColor.GOLD; +import static net.kyori.adventure.text.format.NamedTextColor.GRAY; +import static net.kyori.adventure.text.format.NamedTextColor.GREEN; +import static net.kyori.adventure.text.format.NamedTextColor.RED; +import static net.kyori.adventure.text.format.NamedTextColor.WHITE; +import static net.kyori.adventure.text.format.NamedTextColor.YELLOW; +import static net.kyori.adventure.text.format.TextDecoration.BOLD; public class HealthModule implements CommandModule { @@ -57,51 +71,7 @@ public class HealthModule implements CommandModule { public void registerCommands(Consumer<Command> consumer) { consumer.accept(Command.builder() .aliases("tps", "cpu") - .executor((platform, sender, resp, arguments) -> { - TickStatistics tickStatistics = platform.getTickStatistics(); - if (tickStatistics != null) { - resp.replyPrefixed(text("TPS from last 5s, 10s, 1m, 5m, 15m:")); - resp.replyPrefixed(text() - .content(" ") - .append(formatTps(tickStatistics.tps5Sec())).append(text(", ")) - .append(formatTps(tickStatistics.tps10Sec())).append(text(", ")) - .append(formatTps(tickStatistics.tps1Min())).append(text(", ")) - .append(formatTps(tickStatistics.tps5Min())).append(text(", ")) - .append(formatTps(tickStatistics.tps15Min())) - .build() - ); - resp.replyPrefixed(empty()); - - if (tickStatistics.isDurationSupported()) { - resp.replyPrefixed(text("Tick durations (min/med/95%ile/max ms) from last 10s, 1m:")); - resp.replyPrefixed(text() - .content(" ") - .append(formatTickDurations(tickStatistics.duration10Sec())).append(text("; ")) - .append(formatTickDurations(tickStatistics.duration1Min())) - .build() - ); - resp.replyPrefixed(empty()); - } - } - - resp.replyPrefixed(text("CPU usage from last 10s, 1m, 15m:")); - resp.replyPrefixed(text() - .content(" ") - .append(formatCpuUsage(CpuMonitor.systemLoad10SecAvg())).append(text(", ")) - .append(formatCpuUsage(CpuMonitor.systemLoad1MinAvg())).append(text(", ")) - .append(formatCpuUsage(CpuMonitor.systemLoad15MinAvg())) - .append(text(" (system)", DARK_GRAY)) - .build() - ); - resp.replyPrefixed(text() - .content(" ") - .append(formatCpuUsage(CpuMonitor.processLoad10SecAvg())).append(text(", ")) - .append(formatCpuUsage(CpuMonitor.processLoad1MinAvg())).append(text(", ")) - .append(formatCpuUsage(CpuMonitor.processLoad15MinAvg())) - .append(text(" (process)", DARK_GRAY)) - .build() - ); - }) + .executor(HealthModule::tps) .tabCompleter(Command.TabCompleter.empty()) .build() ); @@ -109,197 +79,265 @@ public class HealthModule implements CommandModule { consumer.accept(Command.builder() .aliases("healthreport", "health", "ht") .argumentUsage("memory", null) - .executor((platform, sender, resp, arguments) -> { - resp.replyPrefixed(text("Generating server health report...")); - platform.getPlugin().executeAsync(() -> { - List<Component> report = new LinkedList<>(); - report.add(empty()); - - TickStatistics tickStatistics = platform.getTickStatistics(); - if (tickStatistics != null) { - report.add(text() - .append(text(">", DARK_GRAY, BOLD)) - .append(space()) - .append(text("TPS from last 5s, 10s, 1m, 5m, 15m:", GOLD)) - .build() - ); - report.add(text() - .content(" ") - .append(formatTps(tickStatistics.tps5Sec())).append(text(", ")) - .append(formatTps(tickStatistics.tps10Sec())).append(text(", ")) - .append(formatTps(tickStatistics.tps1Min())).append(text(", ")) - .append(formatTps(tickStatistics.tps5Min())).append(text(", ")) - .append(formatTps(tickStatistics.tps15Min())) - .build() - ); - report.add(empty()); - - if (tickStatistics.isDurationSupported()) { - report.add(text() - .append(text(">", DARK_GRAY, BOLD)) - .append(space()) - .append(text("Tick durations (min/med/95%ile/max ms) from last 10s, 1m:", GOLD)) - .build() - ); - report.add(text() - .content(" ") - .append(formatTickDurations(tickStatistics.duration10Sec())).append(text("; ")) - .append(formatTickDurations(tickStatistics.duration1Min())) - .build() - ); - report.add(empty()); - } - } - - report.add(text() - .append(text(">", DARK_GRAY, BOLD)) - .append(space()) - .append(text("CPU usage from last 10s, 1m, 15m:", GOLD)) - .build() - ); - report.add(text() - .content(" ") - .append(formatCpuUsage(CpuMonitor.systemLoad10SecAvg())).append(text(", ")) - .append(formatCpuUsage(CpuMonitor.systemLoad1MinAvg())).append(text(", ")) - .append(formatCpuUsage(CpuMonitor.systemLoad15MinAvg())) - .append(text(" (system)", DARK_GRAY)) - .build() - ); - report.add(text() - .content(" ") - .append(formatCpuUsage(CpuMonitor.processLoad10SecAvg())).append(text(", ")) - .append(formatCpuUsage(CpuMonitor.processLoad1MinAvg())).append(text(", ")) - .append(formatCpuUsage(CpuMonitor.processLoad15MinAvg())) - .append(text(" (process)", DARK_GRAY)) - .build() - ); - report.add(empty()); - - MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean(); - MemoryUsage heapUsage = memoryMXBean.getHeapMemoryUsage(); - report.add(text() - .append(text(">", DARK_GRAY, BOLD)) - .append(space()) - .append(text("Memory usage:", GOLD)) - .build() - ); - report.add(text() - .content(" ") - .append(text(FormatUtil.formatBytes(heapUsage.getUsed()), WHITE)) - .append(space()) - .append(text("/", GRAY)) - .append(space()) - .append(text(FormatUtil.formatBytes(heapUsage.getMax()), WHITE)) - .append(text(" ")) - .append(text("(", GRAY)) - .append(text(FormatUtil.percent(heapUsage.getUsed(), heapUsage.getMax()), GREEN)) - .append(text(")", GRAY)) - .build() - ); - report.add(text().content(" ").append(generateMemoryUsageDiagram(heapUsage, 40)).build()); - report.add(empty()); - - if (arguments.boolFlag("memory")) { - MemoryUsage nonHeapUsage = memoryMXBean.getNonHeapMemoryUsage(); - report.add(text() - .append(text(">", DARK_GRAY, BOLD)) - .append(space()) - .append(text("Non-heap memory usage:", GOLD)) - .build() - ); - report.add(text() - .content(" ") - .append(text(FormatUtil.formatBytes(nonHeapUsage.getUsed()), WHITE)) - .build() - ); - report.add(empty()); - - List<MemoryPoolMXBean> memoryPoolMXBeans = ManagementFactory.getMemoryPoolMXBeans(); - for (MemoryPoolMXBean memoryPool : memoryPoolMXBeans) { - if (memoryPool.getType() != MemoryType.HEAP) { - continue; - } - - MemoryUsage usage = memoryPool.getUsage(); - MemoryUsage collectionUsage = memoryPool.getCollectionUsage(); - - if (usage.getMax() == -1) { - usage = new MemoryUsage(usage.getInit(), usage.getUsed(), usage.getCommitted(), usage.getCommitted()); - } - - report.add(text() - .append(text(">", DARK_GRAY, BOLD)) - .append(space()) - .append(text(memoryPool.getName() + " pool usage:", GOLD)) - .build() - ); - report.add(text() - .content(" ") - .append(text(FormatUtil.formatBytes(usage.getUsed()), WHITE)) - .append(space()) - .append(text("/", GRAY)) - .append(space()) - .append(text(FormatUtil.formatBytes(usage.getMax()), WHITE)) - .append(text(" ")) - .append(text("(", GRAY)) - .append(text(FormatUtil.percent(usage.getUsed(), usage.getMax()), GREEN)) - .append(text(")", GRAY)) - .build() - ); - report.add(text().content(" ").append(generateMemoryPoolDiagram(usage, collectionUsage, 40)).build()); - - if (collectionUsage != null) { - report.add(text() - .content(" ") - .append(text("-", RED)) - .append(space()) - .append(text("Usage at last GC:", GRAY)) - .append(space()) - .append(text(FormatUtil.formatBytes(collectionUsage.getUsed()), WHITE)) - .build() - ); - } - report.add(empty()); - } - } - - try { - FileStore fileStore = Files.getFileStore(Paths.get(".")); - long totalSpace = fileStore.getTotalSpace(); - long usedSpace = totalSpace - fileStore.getUsableSpace(); - report.add(text() - .append(text(">", DARK_GRAY, BOLD)) - .append(space()) - .append(text("Disk usage:", GOLD)) - .build() - ); - report.add(text() - .content(" ") - .append(text(FormatUtil.formatBytes(usedSpace), WHITE)) - .append(space()) - .append(text("/", GRAY)) - .append(space()) - .append(text(FormatUtil.formatBytes(totalSpace), WHITE)) - .append(text(" ")) - .append(text("(", GRAY)) - .append(text(FormatUtil.percent(usedSpace, totalSpace), GREEN)) - .append(text(")", GRAY)) - .build() - ); - report.add(text().content(" ").append(generateDiskUsageDiagram(usedSpace, totalSpace, 40)).build()); - report.add(empty()); - } catch (IOException e) { - e.printStackTrace(); - } - - report.forEach(resp::reply); - }); - }) + .executor(HealthModule::healthReport) .tabCompleter((platform, sender, arguments) -> TabCompleter.completeForOpts(arguments, "--memory")) .build() ); } + private static void tps(SparkPlatform platform, CommandSender sender, CommandResponseHandler resp, Arguments arguments) { + TickStatistics tickStatistics = platform.getTickStatistics(); + if (tickStatistics != null) { + resp.replyPrefixed(text("TPS from last 5s, 10s, 1m, 5m, 15m:")); + resp.replyPrefixed(text() + .content(" ") + .append(formatTps(tickStatistics.tps5Sec())).append(text(", ")) + .append(formatTps(tickStatistics.tps10Sec())).append(text(", ")) + .append(formatTps(tickStatistics.tps1Min())).append(text(", ")) + .append(formatTps(tickStatistics.tps5Min())).append(text(", ")) + .append(formatTps(tickStatistics.tps15Min())) + .build() + ); + resp.replyPrefixed(empty()); + + if (tickStatistics.isDurationSupported()) { + resp.replyPrefixed(text("Tick durations (min/med/95%ile/max ms) from last 10s, 1m:")); + resp.replyPrefixed(text() + .content(" ") + .append(formatTickDurations(tickStatistics.duration10Sec())).append(text("; ")) + .append(formatTickDurations(tickStatistics.duration1Min())) + .build() + ); + resp.replyPrefixed(empty()); + } + } + + resp.replyPrefixed(text("CPU usage from last 10s, 1m, 15m:")); + resp.replyPrefixed(text() + .content(" ") + .append(formatCpuUsage(CpuMonitor.systemLoad10SecAvg())).append(text(", ")) + .append(formatCpuUsage(CpuMonitor.systemLoad1MinAvg())).append(text(", ")) + .append(formatCpuUsage(CpuMonitor.systemLoad15MinAvg())) + .append(text(" (system)", DARK_GRAY)) + .build() + ); + resp.replyPrefixed(text() + .content(" ") + .append(formatCpuUsage(CpuMonitor.processLoad10SecAvg())).append(text(", ")) + .append(formatCpuUsage(CpuMonitor.processLoad1MinAvg())).append(text(", ")) + .append(formatCpuUsage(CpuMonitor.processLoad15MinAvg())) + .append(text(" (process)", DARK_GRAY)) + .build() + ); + } + + private static void healthReport(SparkPlatform platform, CommandSender sender, CommandResponseHandler resp, Arguments arguments) { + resp.replyPrefixed(text("Generating server health report...")); + platform.getPlugin().executeAsync(() -> { + List<Component> report = new LinkedList<>(); + report.add(empty()); + + TickStatistics tickStatistics = platform.getTickStatistics(); + if (tickStatistics != null) { + addTickStats(report, tickStatistics); + } + + addCpuStats(report); + + MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean(); + addBasicMemoryStats(report, memoryMXBean); + + if (arguments.boolFlag("memory")) { + addDetailedMemoryStats(report, memoryMXBean); + } + + try { + addDiskStats(report); + } catch (IOException e) { + e.printStackTrace(); + } + + report.forEach(resp::reply); + }); + } + + private static void addTickStats(List<Component> report, TickStatistics tickStatistics) { + report.add(text() + .append(text(">", DARK_GRAY, BOLD)) + .append(space()) + .append(text("TPS from last 5s, 10s, 1m, 5m, 15m:", GOLD)) + .build() + ); + report.add(text() + .content(" ") + .append(formatTps(tickStatistics.tps5Sec())).append(text(", ")) + .append(formatTps(tickStatistics.tps10Sec())).append(text(", ")) + .append(formatTps(tickStatistics.tps1Min())).append(text(", ")) + .append(formatTps(tickStatistics.tps5Min())).append(text(", ")) + .append(formatTps(tickStatistics.tps15Min())) + .build() + ); + report.add(empty()); + + if (tickStatistics.isDurationSupported()) { + report.add(text() + .append(text(">", DARK_GRAY, BOLD)) + .append(space()) + .append(text("Tick durations (min/med/95%ile/max ms) from last 10s, 1m:", GOLD)) + .build() + ); + report.add(text() + .content(" ") + .append(formatTickDurations(tickStatistics.duration10Sec())).append(text("; ")) + .append(formatTickDurations(tickStatistics.duration1Min())) + .build() + ); + report.add(empty()); + } + } + + private static void addCpuStats(List<Component> report) { + report.add(text() + .append(text(">", DARK_GRAY, BOLD)) + .append(space()) + .append(text("CPU usage from last 10s, 1m, 15m:", GOLD)) + .build() + ); + report.add(text() + .content(" ") + .append(formatCpuUsage(CpuMonitor.systemLoad10SecAvg())).append(text(", ")) + .append(formatCpuUsage(CpuMonitor.systemLoad1MinAvg())).append(text(", ")) + .append(formatCpuUsage(CpuMonitor.systemLoad15MinAvg())) + .append(text(" (system)", DARK_GRAY)) + .build() + ); + report.add(text() + .content(" ") + .append(formatCpuUsage(CpuMonitor.processLoad10SecAvg())).append(text(", ")) + .append(formatCpuUsage(CpuMonitor.processLoad1MinAvg())).append(text(", ")) + .append(formatCpuUsage(CpuMonitor.processLoad15MinAvg())) + .append(text(" (process)", DARK_GRAY)) + .build() + ); + report.add(empty()); + } + + private static void addBasicMemoryStats(List<Component> report, MemoryMXBean memoryMXBean) { + MemoryUsage heapUsage = memoryMXBean.getHeapMemoryUsage(); + report.add(text() + .append(text(">", DARK_GRAY, BOLD)) + .append(space()) + .append(text("Memory usage:", GOLD)) + .build() + ); + report.add(text() + .content(" ") + .append(text(FormatUtil.formatBytes(heapUsage.getUsed()), WHITE)) + .append(space()) + .append(text("/", GRAY)) + .append(space()) + .append(text(FormatUtil.formatBytes(heapUsage.getMax()), WHITE)) + .append(text(" ")) + .append(text("(", GRAY)) + .append(text(FormatUtil.percent(heapUsage.getUsed(), heapUsage.getMax()), GREEN)) + .append(text(")", GRAY)) + .build() + ); + report.add(text().content(" ").append(generateMemoryUsageDiagram(heapUsage, 40)).build()); + report.add(empty()); + } + + private static void addDetailedMemoryStats(List<Component> report, MemoryMXBean memoryMXBean) { + MemoryUsage nonHeapUsage = memoryMXBean.getNonHeapMemoryUsage(); + report.add(text() + .append(text(">", DARK_GRAY, BOLD)) + .append(space()) + .append(text("Non-heap memory usage:", GOLD)) + .build() + ); + report.add(text() + .content(" ") + .append(text(FormatUtil.formatBytes(nonHeapUsage.getUsed()), WHITE)) + .build() + ); + report.add(empty()); + + List<MemoryPoolMXBean> memoryPoolMXBeans = ManagementFactory.getMemoryPoolMXBeans(); + for (MemoryPoolMXBean memoryPool : memoryPoolMXBeans) { + if (memoryPool.getType() != MemoryType.HEAP) { + continue; + } + + MemoryUsage usage = memoryPool.getUsage(); + MemoryUsage collectionUsage = memoryPool.getCollectionUsage(); + + if (usage.getMax() == -1) { + usage = new MemoryUsage(usage.getInit(), usage.getUsed(), usage.getCommitted(), usage.getCommitted()); + } + + report.add(text() + .append(text(">", DARK_GRAY, BOLD)) + .append(space()) + .append(text(memoryPool.getName() + " pool usage:", GOLD)) + .build() + ); + report.add(text() + .content(" ") + .append(text(FormatUtil.formatBytes(usage.getUsed()), WHITE)) + .append(space()) + .append(text("/", GRAY)) + .append(space()) + .append(text(FormatUtil.formatBytes(usage.getMax()), WHITE)) + .append(text(" ")) + .append(text("(", GRAY)) + .append(text(FormatUtil.percent(usage.getUsed(), usage.getMax()), GREEN)) + .append(text(")", GRAY)) + .build() + ); + report.add(text().content(" ").append(generateMemoryPoolDiagram(usage, collectionUsage, 40)).build()); + + if (collectionUsage != null) { + report.add(text() + .content(" ") + .append(text("-", RED)) + .append(space()) + .append(text("Usage at last GC:", GRAY)) + .append(space()) + .append(text(FormatUtil.formatBytes(collectionUsage.getUsed()), WHITE)) + .build() + ); + } + report.add(empty()); + } + } + + private static void addDiskStats(List<Component> report) throws IOException { + FileStore fileStore = Files.getFileStore(Paths.get(".")); + long totalSpace = fileStore.getTotalSpace(); + long usedSpace = totalSpace - fileStore.getUsableSpace(); + report.add(text() + .append(text(">", DARK_GRAY, BOLD)) + .append(space()) + .append(text("Disk usage:", GOLD)) + .build() + ); + report.add(text() + .content(" ") + .append(text(FormatUtil.formatBytes(usedSpace), WHITE)) + .append(space()) + .append(text("/", GRAY)) + .append(space()) + .append(text(FormatUtil.formatBytes(totalSpace), WHITE)) + .append(text(" ")) + .append(text("(", GRAY)) + .append(text(FormatUtil.percent(usedSpace, totalSpace), GREEN)) + .append(text(")", GRAY)) + .build() + ); + report.add(text().content(" ").append(generateDiskUsageDiagram(usedSpace, totalSpace, 40)).build()); + report.add(empty()); + } + public static TextComponent formatTps(double tps) { TextColor color; if (tps > 18.0) { @@ -313,7 +351,7 @@ public class HealthModule implements CommandModule { return text((tps > 20.0 ? "*" : "") + Math.min(Math.round(tps * 100.0) / 100.0, 20.0), color); } - public static TextComponent formatTickDurations(RollingAverage average){ + public static TextComponent formatTickDurations(RollingAverage average) { return text() .append(formatTickDuration(average.getMin())) .append(text('/', GRAY)) @@ -325,7 +363,7 @@ public class HealthModule implements CommandModule { .build(); } - public static TextComponent formatTickDuration(double duration){ + public static TextComponent formatTickDuration(double duration) { TextColor color; if (duration >= 50d) { color = RED; diff --git a/spark-common/src/main/java/me/lucko/spark/common/command/modules/HeapAnalysisModule.java b/spark-common/src/main/java/me/lucko/spark/common/command/modules/HeapAnalysisModule.java index 39cb8a3..94e44a6 100644 --- a/spark-common/src/main/java/me/lucko/spark/common/command/modules/HeapAnalysisModule.java +++ b/spark-common/src/main/java/me/lucko/spark/common/command/modules/HeapAnalysisModule.java @@ -21,19 +21,25 @@ package me.lucko.spark.common.command.modules; import me.lucko.spark.common.SparkPlatform; -import me.lucko.spark.common.activitylog.ActivityLog.Activity; +import me.lucko.spark.common.activitylog.Activity; +import me.lucko.spark.common.command.Arguments; 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.command.sender.CommandSender; import me.lucko.spark.common.command.tabcomplete.TabCompleter; import me.lucko.spark.common.heapdump.HeapDump; import me.lucko.spark.common.heapdump.HeapDumpSummary; import me.lucko.spark.common.util.FormatUtil; + import net.kyori.adventure.text.event.ClickEvent; -import okhttp3.MediaType; + import org.tukaani.xz.LZMA2Options; import org.tukaani.xz.LZMAOutputStream; import org.tukaani.xz.XZOutputStream; +import okhttp3.MediaType; + import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -48,8 +54,11 @@ import java.util.function.Consumer; import java.util.function.LongConsumer; import java.util.zip.GZIPOutputStream; -import static net.kyori.adventure.text.Component.*; -import static net.kyori.adventure.text.format.NamedTextColor.*; +import static net.kyori.adventure.text.Component.text; +import static net.kyori.adventure.text.format.NamedTextColor.GOLD; +import static net.kyori.adventure.text.format.NamedTextColor.GRAY; +import static net.kyori.adventure.text.format.NamedTextColor.GREEN; +import static net.kyori.adventure.text.format.NamedTextColor.RED; public class HeapAnalysisModule implements CommandModule { private static final MediaType SPARK_HEAP_MEDIA_TYPE = MediaType.parse("application/x-spark-heap"); @@ -59,44 +68,7 @@ public class HeapAnalysisModule implements CommandModule { consumer.accept(Command.builder() .aliases("heapsummary") .argumentUsage("run-gc-before", null) - .executor((platform, sender, resp, arguments) -> { - platform.getPlugin().executeAsync(() -> { - if (arguments.boolFlag("run-gc-before")) { - resp.broadcastPrefixed(text("Running garbage collector...")); - System.gc(); - } - - resp.broadcastPrefixed(text("Creating a new heap dump summary, please wait...")); - - HeapDumpSummary heapDump; - try { - heapDump = HeapDumpSummary.createNew(); - } catch (Exception e) { - resp.broadcastPrefixed(text("An error occurred whilst inspecting the heap.", RED)); - e.printStackTrace(); - return; - } - - byte[] output = heapDump.formCompressedDataPayload(platform.getPlugin().getPlatformInfo(), sender); - try { - String key = SparkPlatform.BYTEBIN_CLIENT.postContent(output, SPARK_HEAP_MEDIA_TYPE, false).key(); - String url = SparkPlatform.VIEWER_URL + key; - - resp.broadcastPrefixed(text("Heap dump summmary output:", GOLD)); - resp.broadcast(text() - .content(url) - .color(GRAY) - .clickEvent(ClickEvent.openUrl(url)) - .build() - ); - - platform.getActivityLog().addToLog(Activity.urlActivity(sender, System.currentTimeMillis(), "Heap dump summary", url)); - } catch (IOException e) { - resp.broadcastPrefixed(text("An error occurred whilst uploading the data.", RED)); - e.printStackTrace(); - } - }); - }) + .executor(HeapAnalysisModule::heapSummary) .tabCompleter((platform, sender, arguments) -> TabCompleter.completeForOpts(arguments, "--run-gc-before")) .build() ); @@ -104,107 +76,153 @@ public class HeapAnalysisModule implements CommandModule { consumer.accept(Command.builder() .aliases("heapdump") .argumentUsage("compress", "type") - .executor((platform, sender, resp, arguments) -> { - platform.getPlugin().executeAsync(() -> { - Path pluginFolder = platform.getPlugin().getPluginDirectory(); - try { - Files.createDirectories(pluginFolder); - } catch (IOException e) { - // ignore - } + .executor(HeapAnalysisModule::heapDump) + .tabCompleter((platform, sender, arguments) -> TabCompleter.completeForOpts(arguments, "--compress", "--run-gc-before", "--include-non-live")) + .build() + ); + } - Path file = pluginFolder.resolve("heap-" + DateTimeFormatter.ofPattern("yyyy-MM-dd_HH.mm.ss").format(LocalDateTime.now()) + (HeapDump.isOpenJ9() ? ".phd" : ".hprof")); - boolean liveOnly = !arguments.boolFlag("include-non-live"); + private static void heapSummary(SparkPlatform platform, CommandSender sender, CommandResponseHandler resp, Arguments arguments) { + platform.getPlugin().executeAsync(() -> { + if (arguments.boolFlag("run-gc-before")) { + resp.broadcastPrefixed(text("Running garbage collector...")); + System.gc(); + } - if (arguments.boolFlag("run-gc-before")) { - resp.broadcastPrefixed(text("Running garbage collector...")); - System.gc(); - } + resp.broadcastPrefixed(text("Creating a new heap dump summary, please wait...")); - resp.broadcastPrefixed(text("Creating a new heap dump, please wait...")); + HeapDumpSummary heapDump; + try { + heapDump = HeapDumpSummary.createNew(); + } catch (Exception e) { + resp.broadcastPrefixed(text("An error occurred whilst inspecting the heap.", RED)); + e.printStackTrace(); + return; + } - try { - HeapDump.dumpHeap(file, liveOnly); - } catch (Exception e) { - resp.broadcastPrefixed(text("An error occurred whilst creating a heap dump.", RED)); - e.printStackTrace(); - return; - } + byte[] output = heapDump.formCompressedDataPayload(platform.getPlugin().getPlatformInfo(), sender); + try { + String key = SparkPlatform.BYTEBIN_CLIENT.postContent(output, SPARK_HEAP_MEDIA_TYPE).key(); + String url = SparkPlatform.VIEWER_URL + key; - resp.broadcastPrefixed(text() - .content("Heap dump written to: ") - .color(GOLD) - .append(text(file.toString(), GRAY)) - .build() - ); - platform.getActivityLog().addToLog(Activity.fileActivity(sender, System.currentTimeMillis(), "Heap dump", file.toString())); - - - CompressionMethod compress = null; - Iterator<String> compressArgs = arguments.stringFlag("compress").iterator(); - if (compressArgs.hasNext()) { - try { - compress = CompressionMethod.valueOf(compressArgs.next().toUpperCase()); - } catch (IllegalArgumentException e) { - // ignore - } - } + resp.broadcastPrefixed(text("Heap dump summmary output:", GOLD)); + resp.broadcast(text() + .content(url) + .color(GRAY) + .clickEvent(ClickEvent.openUrl(url)) + .build() + ); - if (compress != null) { - resp.broadcastPrefixed(text("Compressing heap dump, please wait...")); - try { - long size = Files.size(file); - AtomicLong lastReport = new AtomicLong(System.currentTimeMillis()); - - LongConsumer progressHandler = progress -> { - long timeSinceLastReport = System.currentTimeMillis() - lastReport.get(); - if (timeSinceLastReport > TimeUnit.SECONDS.toMillis(5)) { - lastReport.set(System.currentTimeMillis()); - - platform.getPlugin().executeAsync(() -> { - resp.broadcastPrefixed(text() - .color(GRAY) - .append(text("Compressed ")) - .append(text(FormatUtil.formatBytes(progress), GOLD)) - .append(text(" / ")) - .append(text(FormatUtil.formatBytes(size), GOLD)) - .append(text(" so far... (")) - .append(text(FormatUtil.percent(progress, size), GREEN)) - .append(text(")")) - .build() - ); - }); - } - }; - - Path compressedFile = compress.compress(file, progressHandler); - long compressedSize = Files.size(compressedFile); - - resp.broadcastPrefixed(text() - .color(GRAY) - .append(text("Compression complete: ")) - .append(text(FormatUtil.formatBytes(size), GOLD)) - .append(text(" --> ")) - .append(text(FormatUtil.formatBytes(compressedSize), GOLD)) - .append(text(" (")) - .append(text(FormatUtil.percent(compressedSize, size), GREEN)) - .append(text(")")) - .build() - ); - - resp.broadcastPrefixed(text() - .content("Compressed heap dump written to: ") - .color(GOLD) - .append(text(compressedFile.toString(), GRAY)) - .build() - ); - } catch (IOException e) { - e.printStackTrace(); - } - } - }); - }) - .tabCompleter((platform, sender, arguments) -> TabCompleter.completeForOpts(arguments, "--compress", "--run-gc-before", "--include-non-live")) + platform.getActivityLog().addToLog(Activity.urlActivity(sender, System.currentTimeMillis(), "Heap dump summary", url)); + } catch (IOException e) { + resp.broadcastPrefixed(text("An error occurred whilst uploading the data.", RED)); + e.printStackTrace(); + } + }); + } + + private static void heapDump(SparkPlatform platform, CommandSender sender, CommandResponseHandler resp, Arguments arguments) { + platform.getPlugin().executeAsync(() -> { + Path pluginFolder = platform.getPlugin().getPluginDirectory(); + try { + Files.createDirectories(pluginFolder); + } catch (IOException e) { + // ignore + } + + Path file = pluginFolder.resolve("heap-" + DateTimeFormatter.ofPattern("yyyy-MM-dd_HH.mm.ss").format(LocalDateTime.now()) + (HeapDump.isOpenJ9() ? ".phd" : ".hprof")); + boolean liveOnly = !arguments.boolFlag("include-non-live"); + + if (arguments.boolFlag("run-gc-before")) { + resp.broadcastPrefixed(text("Running garbage collector...")); + System.gc(); + } + + resp.broadcastPrefixed(text("Creating a new heap dump, please wait...")); + + try { + HeapDump.dumpHeap(file, liveOnly); + } catch (Exception e) { + resp.broadcastPrefixed(text("An error occurred whilst creating a heap dump.", RED)); + e.printStackTrace(); + return; + } + + resp.broadcastPrefixed(text() + .content("Heap dump written to: ") + .color(GOLD) + .append(text(file.toString(), GRAY)) + .build() + ); + platform.getActivityLog().addToLog(Activity.fileActivity(sender, System.currentTimeMillis(), "Heap dump", file.toString())); + + + CompressionMethod compressionMethod = null; + Iterator<String> compressArgs = arguments.stringFlag("compress").iterator(); + if (compressArgs.hasNext()) { + try { + compressionMethod = CompressionMethod.valueOf(compressArgs.next().toUpperCase()); + } catch (IllegalArgumentException e) { + // ignore + } + } + + if (compressionMethod != null) { + try { + heapDumpCompress(platform, resp, file, compressionMethod); + } catch (IOException e) { + e.printStackTrace(); + } + } + }); + } + + private static void heapDumpCompress(SparkPlatform platform, CommandResponseHandler resp, Path file, CompressionMethod method) throws IOException { + resp.broadcastPrefixed(text("Compressing heap dump, please wait...")); + + long size = Files.size(file); + AtomicLong lastReport = new AtomicLong(System.currentTimeMillis()); + + LongConsumer progressHandler = progress -> { + long timeSinceLastReport = System.currentTimeMillis() - lastReport.get(); + if (timeSinceLastReport > TimeUnit.SECONDS.toMillis(5)) { + lastReport.set(System.currentTimeMillis()); + + platform.getPlugin().executeAsync(() -> { + resp.broadcastPrefixed(text() + .color(GRAY) + .append(text("Compressed ")) + .append(text(FormatUtil.formatBytes(progress), GOLD)) + .append(text(" / ")) + .append(text(FormatUtil.formatBytes(size), GOLD)) + .append(text(" so far... (")) + .append(text(FormatUtil.percent(progress, size), GREEN)) + .append(text(")")) + .build() + ); + }); + } + }; + + Path compressedFile = method.compress(file, progressHandler); + long compressedSize = Files.size(compressedFile); + + resp.broadcastPrefixed(text() + .color(GRAY) + .append(text("Compression complete: ")) + .append(text(FormatUtil.formatBytes(size), GOLD)) + .append(text(" --> ")) + .append(text(FormatUtil.formatBytes(compressedSize), GOLD)) + .append(text(" (")) + .append(text(FormatUtil.percent(compressedSize, size), GREEN)) + .append(text(")")) + .build() + ); + + resp.broadcastPrefixed(text() + .content("Compressed heap dump written to: ") + .color(GOLD) + .append(text(compressedFile.toString(), GRAY)) .build() ); } diff --git a/spark-common/src/main/java/me/lucko/spark/common/command/modules/SamplerModule.java b/spark-common/src/main/java/me/lucko/spark/common/command/modules/SamplerModule.java index 094b398..ff577d5 100644 --- a/spark-common/src/main/java/me/lucko/spark/common/command/modules/SamplerModule.java +++ b/spark-common/src/main/java/me/lucko/spark/common/command/modules/SamplerModule.java @@ -21,11 +21,14 @@ package me.lucko.spark.common.command.modules; import com.google.common.collect.Iterables; + import me.lucko.spark.common.SparkPlatform; -import me.lucko.spark.common.activitylog.ActivityLog.Activity; +import me.lucko.spark.common.activitylog.Activity; +import me.lucko.spark.common.command.Arguments; 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.command.sender.CommandSender; import me.lucko.spark.common.command.tabcomplete.CompletionSupplier; import me.lucko.spark.common.command.tabcomplete.TabCompleter; import me.lucko.spark.common.sampler.Sampler; @@ -35,9 +38,11 @@ import me.lucko.spark.common.sampler.ThreadGrouper; import me.lucko.spark.common.sampler.ThreadNodeOrder; import me.lucko.spark.common.sampler.async.AsyncSampler; import me.lucko.spark.common.sampler.node.MergeMode; -import me.lucko.spark.common.sampler.tick.TickHook; +import me.lucko.spark.common.tick.TickHook; import me.lucko.spark.common.util.MethodDisambiguator; + import net.kyori.adventure.text.event.ClickEvent; + import okhttp3.MediaType; import java.io.IOException; @@ -50,8 +55,12 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; -import static net.kyori.adventure.text.Component.*; -import static net.kyori.adventure.text.format.NamedTextColor.*; +import static net.kyori.adventure.text.Component.space; +import static net.kyori.adventure.text.Component.text; +import static net.kyori.adventure.text.format.NamedTextColor.DARK_GRAY; +import static net.kyori.adventure.text.format.NamedTextColor.GOLD; +import static net.kyori.adventure.text.format.NamedTextColor.GRAY; +import static net.kyori.adventure.text.format.NamedTextColor.RED; public class SamplerModule implements CommandModule { private static final MediaType SPARK_SAMPLER_MEDIA_TYPE = MediaType.parse("application/x-spark-sampler"); @@ -84,170 +93,7 @@ public class SamplerModule implements CommandModule { .argumentUsage("force-java-sampler", null) .argumentUsage("stop --comment", "comment") .argumentUsage("stop --order-by-time", null) - .executor((platform, sender, resp, arguments) -> { - if (arguments.boolFlag("info")) { - if (this.activeSampler == null) { - resp.replyPrefixed(text("There isn't an active profiler running.")); - } else { - long timeout = this.activeSampler.getEndTime(); - if (timeout == -1) { - resp.replyPrefixed(text("There is an active profiler currently running, with no defined timeout.")); - } else { - long timeoutDiff = (timeout - System.currentTimeMillis()) / 1000L; - resp.replyPrefixed(text("There is an active profiler currently running, due to timeout in " + timeoutDiff + " seconds.")); - } - - long runningTime = (System.currentTimeMillis() - this.activeSampler.getStartTime()) / 1000L; - resp.replyPrefixed(text("It has been profiling for " + runningTime + " seconds so far.")); - } - return; - } - - if (arguments.boolFlag("cancel")) { - if (this.activeSampler == null) { - resp.replyPrefixed(text("There isn't an active profiler running.")); - } else { - close(); - resp.broadcastPrefixed(text("The active profiler has been cancelled.", GOLD)); - } - return; - } - - if (arguments.boolFlag("stop") || arguments.boolFlag("upload")) { - if (this.activeSampler == null) { - resp.replyPrefixed(text("There isn't an active profiler running.")); - } else { - this.activeSampler.stop(); - resp.broadcastPrefixed(text("The active profiler has been stopped! Uploading results...")); - ThreadNodeOrder threadOrder = arguments.boolFlag("order-by-time") ? ThreadNodeOrder.BY_TIME : ThreadNodeOrder.BY_NAME; - String comment = Iterables.getFirst(arguments.stringFlag("comment"), null); - MethodDisambiguator methodDisambiguator = new MethodDisambiguator(); - MergeMode mergeMode = arguments.boolFlag("separate-parent-calls") ? MergeMode.separateParentCalls(methodDisambiguator) : MergeMode.sameMethod(methodDisambiguator); - handleUpload(platform, resp, this.activeSampler, threadOrder, comment, mergeMode); - this.activeSampler = null; - } - return; - } - - int timeoutSeconds = arguments.intFlag("timeout"); - if (timeoutSeconds != -1 && timeoutSeconds <= 10) { - resp.replyPrefixed(text("The specified timeout is not long enough for accurate results to be formed. " + - "Please choose a value greater than 10.", RED)); - return; - } - - if (timeoutSeconds != -1 && timeoutSeconds < 30) { - resp.replyPrefixed(text("The accuracy of the output will significantly improve when the profiler is able to run for longer periods. " + - "Consider setting a timeout value over 30 seconds.")); - } - - double intervalMillis = arguments.doubleFlag("interval"); - if (intervalMillis <= 0) { - intervalMillis = 4; - } - - boolean ignoreSleeping = arguments.boolFlag("ignore-sleeping"); - boolean ignoreNative = arguments.boolFlag("ignore-native"); - boolean forceJavaSampler = arguments.boolFlag("force-java-sampler"); - - Set<String> threads = arguments.stringFlag("thread"); - ThreadDumper threadDumper; - if (threads.isEmpty()) { - // use the server thread - threadDumper = platform.getPlugin().getDefaultThreadDumper(); - } else if (threads.contains("*")) { - threadDumper = ThreadDumper.ALL; - } else { - if (arguments.boolFlag("regex")) { - threadDumper = new ThreadDumper.Regex(threads); - } else { - // specific matches - threadDumper = new ThreadDumper.Specific(threads); - } - } - - ThreadGrouper threadGrouper; - if (arguments.boolFlag("combine-all")) { - threadGrouper = ThreadGrouper.AS_ONE; - } else if (arguments.boolFlag("not-combined")) { - threadGrouper = ThreadGrouper.BY_NAME; - } else { - threadGrouper = ThreadGrouper.BY_POOL; - } - - int ticksOver = arguments.intFlag("only-ticks-over"); - TickHook tickHook = null; - if (ticksOver != -1) { - tickHook = platform.getTickHook(); - if (tickHook == null) { - resp.replyPrefixed(text("Tick counting is not supported!", RED)); - return; - } - } - - if (this.activeSampler != null) { - resp.replyPrefixed(text("An active profiler is already running.")); - return; - } - - resp.broadcastPrefixed(text("Initializing a new profiler, please wait...")); - - SamplerBuilder builder = new SamplerBuilder(); - builder.threadDumper(threadDumper); - builder.threadGrouper(threadGrouper); - if (timeoutSeconds != -1) { - builder.completeAfter(timeoutSeconds, TimeUnit.SECONDS); - } - builder.samplingInterval(intervalMillis); - builder.ignoreSleeping(ignoreSleeping); - builder.ignoreNative(ignoreNative); - builder.forceJavaSampler(forceJavaSampler); - if (ticksOver != -1) { - builder.ticksOver(ticksOver, tickHook); - } - Sampler sampler = this.activeSampler = builder.start(); - - resp.broadcastPrefixed(text() - .append(text("Profiler now active!", GOLD)) - .append(space()) - .append(text("(" + (sampler instanceof AsyncSampler ? "async" : "built-in java") + ")", DARK_GRAY)) - .build() - ); - if (timeoutSeconds == -1) { - resp.broadcastPrefixed(text("Use '/" + platform.getPlugin().getCommandName() + " profiler --stop' to stop profiling and upload the results.")); - } else { - resp.broadcastPrefixed(text("The results will be automatically returned after the profiler has been running for " + timeoutSeconds + " seconds.")); - } - - CompletableFuture<? extends Sampler> future = this.activeSampler.getFuture(); - - // send message if profiling fails - future.whenCompleteAsync((s, throwable) -> { - if (throwable != null) { - resp.broadcastPrefixed(text("Profiler operation failed unexpectedly. Error: " + throwable.toString(), RED)); - throwable.printStackTrace(); - } - }); - - // set activeSampler to null when complete. - future.whenCompleteAsync((s, throwable) -> { - if (sampler == this.activeSampler) { - this.activeSampler = null; - } - }); - - // await the result - if (timeoutSeconds != -1) { - ThreadNodeOrder threadOrder = arguments.boolFlag("order-by-time") ? ThreadNodeOrder.BY_TIME : ThreadNodeOrder.BY_NAME; - String comment = Iterables.getFirst(arguments.stringFlag("comment"), null); - MethodDisambiguator methodDisambiguator = new MethodDisambiguator(); - MergeMode mergeMode = arguments.boolFlag("separate-parent-calls") ? MergeMode.separateParentCalls(methodDisambiguator) : MergeMode.sameMethod(methodDisambiguator); - future.thenAcceptAsync(s -> { - resp.broadcastPrefixed(text("The active profiler has completed! Uploading results...")); - handleUpload(platform, resp, s, threadOrder, comment, mergeMode); - }); - } - }) + .executor(this::profiler) .tabCompleter((platform, sender, arguments) -> { if (arguments.contains("--info") || arguments.contains("--cancel")) { return Collections.emptyList(); @@ -271,11 +117,192 @@ public class SamplerModule implements CommandModule { ); } + private void profiler(SparkPlatform platform, CommandSender sender, CommandResponseHandler resp, Arguments arguments) { + if (arguments.boolFlag("info")) { + profilerInfo(resp); + return; + } + + if (arguments.boolFlag("cancel")) { + profilerCancel(resp); + return; + } + + if (arguments.boolFlag("stop") || arguments.boolFlag("upload")) { + profilerStop(platform, sender, resp, arguments); + return; + } + + profilerStart(platform, sender, resp, arguments); + } + + private void profilerStart(SparkPlatform platform, CommandSender sender, CommandResponseHandler resp, Arguments arguments) { + int timeoutSeconds = arguments.intFlag("timeout"); + if (timeoutSeconds != -1 && timeoutSeconds <= 10) { + resp.replyPrefixed(text("The specified timeout is not long enough for accurate results to be formed. " + + "Please choose a value greater than 10.", RED)); + return; + } + + if (timeoutSeconds != -1 && timeoutSeconds < 30) { + resp.replyPrefixed(text("The accuracy of the output will significantly improve when the profiler is able to run for longer periods. " + + "Consider setting a timeout value over 30 seconds.")); + } + + double intervalMillis = arguments.doubleFlag("interval"); + if (intervalMillis <= 0) { + intervalMillis = 4; + } + + boolean ignoreSleeping = arguments.boolFlag("ignore-sleeping"); + boolean ignoreNative = arguments.boolFlag("ignore-native"); + boolean forceJavaSampler = arguments.boolFlag("force-java-sampler"); + + Set<String> threads = arguments.stringFlag("thread"); + ThreadDumper threadDumper; + if (threads.isEmpty()) { + // use the server thread + threadDumper = platform.getPlugin().getDefaultThreadDumper(); + } else if (threads.contains("*")) { + threadDumper = ThreadDumper.ALL; + } else { + if (arguments.boolFlag("regex")) { + threadDumper = new ThreadDumper.Regex(threads); + } else { + // specific matches + threadDumper = new ThreadDumper.Specific(threads); + } + } + + ThreadGrouper threadGrouper; + if (arguments.boolFlag("combine-all")) { + threadGrouper = ThreadGrouper.AS_ONE; + } else if (arguments.boolFlag("not-combined")) { + threadGrouper = ThreadGrouper.BY_NAME; + } else { + threadGrouper = ThreadGrouper.BY_POOL; + } + + int ticksOver = arguments.intFlag("only-ticks-over"); + TickHook tickHook = null; + if (ticksOver != -1) { + tickHook = platform.getTickHook(); + if (tickHook == null) { + resp.replyPrefixed(text("Tick counting is not supported!", RED)); + return; + } + } + + if (this.activeSampler != null) { + resp.replyPrefixed(text("An active profiler is already running.")); + return; + } + + resp.broadcastPrefixed(text("Initializing a new profiler, please wait...")); + + SamplerBuilder builder = new SamplerBuilder(); + builder.threadDumper(threadDumper); + builder.threadGrouper(threadGrouper); + if (timeoutSeconds != -1) { + builder.completeAfter(timeoutSeconds, TimeUnit.SECONDS); + } + builder.samplingInterval(intervalMillis); + builder.ignoreSleeping(ignoreSleeping); + builder.ignoreNative(ignoreNative); + builder.forceJavaSampler(forceJavaSampler); + if (ticksOver != -1) { + builder.ticksOver(ticksOver, tickHook); + } + Sampler sampler = this.activeSampler = builder.start(); + + resp.broadcastPrefixed(text() + .append(text("Profiler now active!", GOLD)) + .append(space()) + .append(text("(" + (sampler instanceof AsyncSampler ? "async" : "built-in java") + ")", DARK_GRAY)) + .build() + ); + if (timeoutSeconds == -1) { + resp.broadcastPrefixed(text("Use '/" + platform.getPlugin().getCommandName() + " profiler --stop' to stop profiling and upload the results.")); + } else { + resp.broadcastPrefixed(text("The results will be automatically returned after the profiler has been running for " + timeoutSeconds + " seconds.")); + } + + CompletableFuture<? extends Sampler> future = this.activeSampler.getFuture(); + + // send message if profiling fails + future.whenCompleteAsync((s, throwable) -> { + if (throwable != null) { + resp.broadcastPrefixed(text("Profiler operation failed unexpectedly. Error: " + throwable.toString(), RED)); + throwable.printStackTrace(); + } + }); + + // set activeSampler to null when complete. + future.whenCompleteAsync((s, throwable) -> { + if (sampler == this.activeSampler) { + this.activeSampler = null; + } + }); + + // await the result + if (timeoutSeconds != -1) { + ThreadNodeOrder threadOrder = arguments.boolFlag("order-by-time") ? ThreadNodeOrder.BY_TIME : ThreadNodeOrder.BY_NAME; + String comment = Iterables.getFirst(arguments.stringFlag("comment"), null); + MethodDisambiguator methodDisambiguator = new MethodDisambiguator(); + MergeMode mergeMode = arguments.boolFlag("separate-parent-calls") ? MergeMode.separateParentCalls(methodDisambiguator) : MergeMode.sameMethod(methodDisambiguator); + future.thenAcceptAsync(s -> { + resp.broadcastPrefixed(text("The active profiler has completed! Uploading results...")); + handleUpload(platform, resp, s, threadOrder, comment, mergeMode); + }); + } + } + + private void profilerInfo(CommandResponseHandler resp) { + if (this.activeSampler == null) { + resp.replyPrefixed(text("There isn't an active profiler running.")); + } else { + long timeout = this.activeSampler.getEndTime(); + if (timeout == -1) { + resp.replyPrefixed(text("There is an active profiler currently running, with no defined timeout.")); + } else { + long timeoutDiff = (timeout - System.currentTimeMillis()) / 1000L; + resp.replyPrefixed(text("There is an active profiler currently running, due to timeout in " + timeoutDiff + " seconds.")); + } + + long runningTime = (System.currentTimeMillis() - this.activeSampler.getStartTime()) / 1000L; + resp.replyPrefixed(text("It has been profiling for " + runningTime + " seconds so far.")); + } + } + + private void profilerCancel(CommandResponseHandler resp) { + if (this.activeSampler == null) { + resp.replyPrefixed(text("There isn't an active profiler running.")); + } else { + close(); + resp.broadcastPrefixed(text("The active profiler has been cancelled.", GOLD)); + } + } + + private void profilerStop(SparkPlatform platform, CommandSender sender, CommandResponseHandler resp, Arguments arguments) { + if (this.activeSampler == null) { + resp.replyPrefixed(text("There isn't an active profiler running.")); + } else { + this.activeSampler.stop(); + resp.broadcastPrefixed(text("The active profiler has been stopped! Uploading results...")); + ThreadNodeOrder threadOrder = arguments.boolFlag("order-by-time") ? ThreadNodeOrder.BY_TIME : ThreadNodeOrder.BY_NAME; + String comment = Iterables.getFirst(arguments.stringFlag("comment"), null); + MethodDisambiguator methodDisambiguator = new MethodDisambiguator(); + MergeMode mergeMode = arguments.boolFlag("separate-parent-calls") ? MergeMode.separateParentCalls(methodDisambiguator) : MergeMode.sameMethod(methodDisambiguator); + handleUpload(platform, resp, this.activeSampler, threadOrder, comment, mergeMode); + this.activeSampler = null; + } + } + private void handleUpload(SparkPlatform platform, CommandResponseHandler resp, Sampler sampler, ThreadNodeOrder threadOrder, String comment, MergeMode mergeMode) { platform.getPlugin().executeAsync(() -> { byte[] output = sampler.formCompressedDataPayload(platform.getPlugin().getPlatformInfo(), resp.sender(), threadOrder, comment, mergeMode); try { - String key = SparkPlatform.BYTEBIN_CLIENT.postContent(output, SPARK_SAMPLER_MEDIA_TYPE, false).key(); + String key = SparkPlatform.BYTEBIN_CLIENT.postContent(output, SPARK_SAMPLER_MEDIA_TYPE).key(); String url = SparkPlatform.VIEWER_URL + key; resp.broadcastPrefixed(text("Profiler results:", GOLD)); diff --git a/spark-common/src/main/java/me/lucko/spark/common/command/modules/TickMonitoringModule.java b/spark-common/src/main/java/me/lucko/spark/common/command/modules/TickMonitoringModule.java index 043bc65..f5f4fce 100644 --- a/spark-common/src/main/java/me/lucko/spark/common/command/modules/TickMonitoringModule.java +++ b/spark-common/src/main/java/me/lucko/spark/common/command/modules/TickMonitoringModule.java @@ -25,26 +25,25 @@ 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.command.tabcomplete.TabCompleter; +import me.lucko.spark.common.monitor.tick.ReportPredicate; import me.lucko.spark.common.monitor.tick.TickMonitor; -import me.lucko.spark.common.monitor.tick.TickMonitor.ReportPredicate; -import me.lucko.spark.common.sampler.tick.TickHook; +import me.lucko.spark.common.tick.TickHook; + import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; import java.util.function.Consumer; -import static net.kyori.adventure.text.Component.*; +import static net.kyori.adventure.text.Component.text; public class TickMonitoringModule implements CommandModule { /** The tick hook instance currently running, if any */ - private TickHook tickHook = null; - private ReportingTickMonitor activeTickMonitor = null; + private TickMonitor activeTickMonitor = null; @Override public void close() { if (this.activeTickMonitor != null) { - this.tickHook.removeCallback(this.activeTickMonitor); this.activeTickMonitor.close(); this.activeTickMonitor = null; } @@ -58,10 +57,8 @@ public class TickMonitoringModule implements CommandModule { .argumentUsage("threshold-tick", "tick duration") .argumentUsage("without-gc", null) .executor((platform, sender, resp, arguments) -> { - if (this.tickHook == null) { - this.tickHook = platform.getTickHook(); - } - if (this.tickHook == null) { + TickHook tickHook = platform.getTickHook(); + if (tickHook == null) { resp.replyPrefixed(text("Not supported!", NamedTextColor.RED)); return; } @@ -78,8 +75,8 @@ public class TickMonitoringModule implements CommandModule { reportPredicate = new ReportPredicate.PercentageChangeGt(100); } - this.activeTickMonitor = new ReportingTickMonitor(platform, resp, this.tickHook, reportPredicate, !arguments.boolFlag("without-gc")); - this.tickHook.addCallback(this.activeTickMonitor); + this.activeTickMonitor = new ReportingTickMonitor(platform, resp, tickHook, reportPredicate, !arguments.boolFlag("without-gc")); + this.activeTickMonitor.start(); } else { close(); resp.broadcastPrefixed(text("Tick monitor disabled.")); diff --git a/spark-common/src/main/java/me/lucko/spark/common/command/sender/CommandSender.java b/spark-common/src/main/java/me/lucko/spark/common/command/sender/CommandSender.java index b10c7d8..9feeac2 100644 --- a/spark-common/src/main/java/me/lucko/spark/common/command/sender/CommandSender.java +++ b/spark-common/src/main/java/me/lucko/spark/common/command/sender/CommandSender.java @@ -23,7 +23,9 @@ package me.lucko.spark.common.command.sender; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonPrimitive; + import me.lucko.spark.proto.SparkProtos.CommandSenderData; + import net.kyori.adventure.text.Component; import java.util.UUID; diff --git a/spark-common/src/main/java/me/lucko/spark/common/heapdump/HeapDump.java b/spark-common/src/main/java/me/lucko/spark/common/heapdump/HeapDump.java index 975dbb3..955bafe 100644 --- a/spark-common/src/main/java/me/lucko/spark/common/heapdump/HeapDump.java +++ b/spark-common/src/main/java/me/lucko/spark/common/heapdump/HeapDump.java @@ -49,18 +49,26 @@ public enum HeapDump { String outputPathString = outputPath.toAbsolutePath().normalize().toString(); if (isOpenJ9()) { - Class<?> dumpClass = Class.forName("com.ibm.jvm.Dump"); - Method heapDumpMethod = dumpClass.getMethod("heapDumpToFile", String.class); - heapDumpMethod.invoke(null, outputPathString); + dumpOpenJ9(outputPathString); } else { - MBeanServer beanServer = ManagementFactory.getPlatformMBeanServer(); - ObjectName diagnosticBeanName = ObjectName.getInstance(DIAGNOSTIC_BEAN); - - HotSpotDiagnosticMXBean proxy = JMX.newMXBeanProxy(beanServer, diagnosticBeanName, HotSpotDiagnosticMXBean.class); - proxy.dumpHeap(outputPathString, live); + dumpHotspot(outputPathString, live); } } + private static void dumpOpenJ9(String outputPathString) throws Exception { + Class<?> dumpClass = Class.forName("com.ibm.jvm.Dump"); + Method heapDumpMethod = dumpClass.getMethod("heapDumpToFile", String.class); + heapDumpMethod.invoke(null, outputPathString); + } + + private static void dumpHotspot(String outputPathString, boolean live) throws Exception { + MBeanServer beanServer = ManagementFactory.getPlatformMBeanServer(); + ObjectName diagnosticBeanName = ObjectName.getInstance(DIAGNOSTIC_BEAN); + + HotSpotDiagnosticMXBean proxy = JMX.newMXBeanProxy(beanServer, diagnosticBeanName, HotSpotDiagnosticMXBean.class); + proxy.dumpHeap(outputPathString, live); + } + public static boolean isOpenJ9() { try { Class.forName("com.ibm.jvm.Dump"); diff --git a/spark-common/src/main/java/me/lucko/spark/common/heapdump/HeapDumpSummary.java b/spark-common/src/main/java/me/lucko/spark/common/heapdump/HeapDumpSummary.java index 61ffd71..a1ad819 100644 --- a/spark-common/src/main/java/me/lucko/spark/common/heapdump/HeapDumpSummary.java +++ b/spark-common/src/main/java/me/lucko/spark/common/heapdump/HeapDumpSummary.java @@ -25,6 +25,7 @@ import me.lucko.spark.common.platform.PlatformInfo; import me.lucko.spark.proto.SparkProtos; import me.lucko.spark.proto.SparkProtos.HeapData; import me.lucko.spark.proto.SparkProtos.HeapEntry; + import org.objectweb.asm.Type; import java.io.ByteArrayOutputStream; diff --git a/spark-common/src/main/java/me/lucko/spark/common/monitor/memory/GarbageCollectionMonitor.java b/spark-common/src/main/java/me/lucko/spark/common/monitor/memory/GarbageCollectionMonitor.java index f66d43b..9bff1e2 100644 --- a/spark-common/src/main/java/me/lucko/spark/common/monitor/memory/GarbageCollectionMonitor.java +++ b/spark-common/src/main/java/me/lucko/spark/common/monitor/memory/GarbageCollectionMonitor.java @@ -33,21 +33,28 @@ import javax.management.NotificationEmitter; import javax.management.NotificationListener; import javax.management.openmbean.CompositeData; +/** + * Monitoring process for garbage collections. + */ public class GarbageCollectionMonitor implements NotificationListener, AutoCloseable { + /** The registered listeners */ private final List<Listener> listeners = new ArrayList<>(); + /** A list of the NotificationEmitters that feed information to this monitor. */ private final List<NotificationEmitter> emitters = new ArrayList<>(); public GarbageCollectionMonitor() { - List<GarbageCollectorMXBean> beans = ManagementFactory.getGarbageCollectorMXBeans(); - for (GarbageCollectorMXBean bean : beans) { - if (!(bean instanceof NotificationEmitter)) { - continue; - } + // Add ourselves as a notification listener for all GarbageCollectorMXBean that + // support notifications. + for (GarbageCollectorMXBean bean : ManagementFactory.getGarbageCollectorMXBeans()) { + if (bean instanceof NotificationEmitter) { + NotificationEmitter notificationEmitter = (NotificationEmitter) bean; + notificationEmitter.addNotificationListener(this, null, null); - NotificationEmitter notificationEmitter = (NotificationEmitter) bean; - notificationEmitter.addNotificationListener(this, null, null); - this.emitters.add(notificationEmitter); + // Keep track of the notification emitters we subscribe to so + // the listeners can be removed on #close + this.emitters.add(notificationEmitter); + } } } @@ -61,6 +68,7 @@ public class GarbageCollectionMonitor implements NotificationListener, AutoClose @Override public void handleNotification(Notification notification, Object handback) { + // we're only interested in GC notifications if (!notification.getType().equals(GarbageCollectionNotificationInfo.GARBAGE_COLLECTION_NOTIFICATION)) { return; } @@ -84,8 +92,27 @@ public class GarbageCollectionMonitor implements NotificationListener, AutoClose this.listeners.clear(); } + /** + * A simple listener object for garbage collections. + */ public interface Listener { void onGc(GarbageCollectionNotificationInfo data); } + /** + * Gets a human-friendly description for the type of the given GC notification. + * + * @param info the notification object + * @return the name of the GC type + */ + public static String getGcType(GarbageCollectionNotificationInfo info) { + if (info.getGcAction().equals("end of minor GC")) { + return "Young Gen"; + } else if (info.getGcAction().equals("end of major GC")) { + return "Old Gen"; + } else { + return info.getGcAction(); + } + } + } 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 index e875fd7..c831ea1 100644 --- 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 @@ -32,6 +32,11 @@ import java.util.Map; public class GarbageCollectorStatistics { public static final GarbageCollectorStatistics ZERO = new GarbageCollectorStatistics(0, 0); + /** + * Polls a set of statistics from the {@link GarbageCollectorMXBean}. + * + * @return the polled statistics + */ public static Map<String, GarbageCollectorStatistics> pollStats() { ImmutableMap.Builder<String, GarbageCollectorStatistics> stats = ImmutableMap.builder(); for (GarbageCollectorMXBean bean : ManagementFactory.getGarbageCollectorMXBeans()) { @@ -40,6 +45,15 @@ public class GarbageCollectorStatistics { return stats.build(); } + /** + * Polls a set of statistics from the {@link GarbageCollectorMXBean}, then subtracts + * {@code initial} from them. + * + * <p>The reason for subtracting the initial statistics is to ignore GC activity + * that took place before the server/client fully started.</p> + * + * @return the polled statistics + */ public static Map<String, GarbageCollectorStatistics> pollStatsSubtractInitial(Map<String, GarbageCollectorStatistics> initial) { ImmutableMap.Builder<String, GarbageCollectorStatistics> stats = ImmutableMap.builder(); for (GarbageCollectorMXBean bean : ManagementFactory.getGarbageCollectorMXBeans()) { diff --git a/spark-common/src/main/java/me/lucko/spark/common/monitor/tick/ReportPredicate.java b/spark-common/src/main/java/me/lucko/spark/common/monitor/tick/ReportPredicate.java new file mode 100644 index 0000000..5d25b09 --- /dev/null +++ b/spark-common/src/main/java/me/lucko/spark/common/monitor/tick/ReportPredicate.java @@ -0,0 +1,90 @@ +/* + * 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.tick; + +import net.kyori.adventure.text.Component; + +/** + * A predicate to test whether a tick should be reported. + */ +public interface ReportPredicate { + + /** + * Tests whether a tick should be reported. + * + * @param duration the tick duration + * @param increaseFromAvg the difference between the ticks duration and the average + * @param percentageChange the percentage change between the ticks duration and the average + * @return true if the tick should be reported, false otherwise + */ + boolean shouldReport(double duration, double increaseFromAvg, double percentageChange); + + /** + * Gets a component to describe how the predicate will select ticks to report. + * + * @return the component + */ + Component monitoringStartMessage(); + + final class PercentageChangeGt implements ReportPredicate { + private final double threshold; + + public PercentageChangeGt(double threshold) { + this.threshold = threshold; + } + + @Override + public boolean shouldReport(double duration, double increaseFromAvg, double percentageChange) { + if (increaseFromAvg <= 0) { + return false; + } + return percentageChange > this.threshold; + } + + @Override + public Component monitoringStartMessage() { + return Component.text("Starting now, any ticks with >" + this.threshold + "% increase in " + + "duration compared to the average will be reported."); + } + } + + final class DurationGt implements ReportPredicate { + private final double threshold; + + public DurationGt(double threshold) { + this.threshold = threshold; + } + + @Override + public boolean shouldReport(double duration, double increaseFromAvg, double percentageChange) { + if (increaseFromAvg <= 0) { + return false; + } + return duration > this.threshold; + } + + @Override + public Component monitoringStartMessage() { + return Component.text("Starting now, any ticks with duration >" + this.threshold + " will be reported."); + } + } + +} diff --git a/spark-common/src/main/java/me/lucko/spark/common/monitor/tick/TickMonitor.java b/spark-common/src/main/java/me/lucko/spark/common/monitor/tick/TickMonitor.java index 6b74cef..944fa83 100644 --- a/spark-common/src/main/java/me/lucko/spark/common/monitor/tick/TickMonitor.java +++ b/spark-common/src/main/java/me/lucko/spark/common/monitor/tick/TickMonitor.java @@ -21,29 +21,59 @@ package me.lucko.spark.common.monitor.tick; import com.sun.management.GarbageCollectionNotificationInfo; + import me.lucko.spark.common.SparkPlatform; import me.lucko.spark.common.monitor.memory.GarbageCollectionMonitor; -import me.lucko.spark.common.sampler.tick.TickHook; +import me.lucko.spark.common.tick.TickHook; + import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.format.NamedTextColor; import java.text.DecimalFormat; import java.util.DoubleSummaryStatistics; +import static net.kyori.adventure.text.Component.space; +import static net.kyori.adventure.text.Component.text; +import static net.kyori.adventure.text.format.NamedTextColor.DARK_GRAY; +import static net.kyori.adventure.text.format.NamedTextColor.GOLD; +import static net.kyori.adventure.text.format.NamedTextColor.GRAY; +import static net.kyori.adventure.text.format.NamedTextColor.RED; +import static net.kyori.adventure.text.format.NamedTextColor.WHITE; + +/** + * Monitoring process for the server/client tick rate. + */ public abstract class TickMonitor implements TickHook.Callback, GarbageCollectionMonitor.Listener, AutoCloseable { - private static final DecimalFormat df = new DecimalFormat("#.##"); + private static final DecimalFormat DF = new DecimalFormat("#.##"); + /** The spark platform */ private final SparkPlatform platform; + /** The tick hook being used as the source for tick information. */ private final TickHook tickHook; + /** The index of the tick when the monitor first started */ private final int zeroTick; + /** The active garbage collection monitor, if enabled */ private final GarbageCollectionMonitor garbageCollectionMonitor; + /** The predicate used to decide if a tick should be reported. */ private final ReportPredicate reportPredicate; - // data + /** + * Enum representing the various phases in a tick monitors lifetime. + */ + private enum Phase { + /** Tick monitor is in the setup phase where it determines the average tick rate. */ + SETUP, + /** Tick monitor is in the monitoring phase where it listens for ticks that exceed the threshold. */ + MONITORING + } + + /** The phase the monitor is in */ + private Phase phase = null; + /** Gets the system timestamp of the last recorded tick */ private volatile double lastTickTime = 0; - private State state = null; - private final DoubleSummaryStatistics averageTickTime = new DoubleSummaryStatistics(); - private double avg; + /** Used to calculate the average tick time during the SETUP phase. */ + private final DoubleSummaryStatistics averageTickTimeCalc = new DoubleSummaryStatistics(); + /** The average tick time, defined at the end of the SETUP phase. */ + private double averageTickTime; public TickMonitor(SparkPlatform platform, TickHook tickHook, ReportPredicate reportPredicate, boolean monitorGc) { this.platform = platform; @@ -65,8 +95,14 @@ public abstract class TickMonitor implements TickHook.Callback, GarbageCollectio protected abstract void sendMessage(Component message); + public void start() { + this.tickHook.addCallback(this); + } + @Override public void close() { + this.tickHook.removeCallback(this); + if (this.garbageCollectionMonitor != null) { this.garbageCollectionMonitor.close(); } @@ -77,10 +113,10 @@ public abstract class TickMonitor implements TickHook.Callback, GarbageCollectio double now = ((double) System.nanoTime()) / 1000000d; // init - if (this.state == null) { - this.state = State.SETUP; + if (this.phase == null) { + this.phase = Phase.SETUP; this.lastTickTime = now; - sendMessage(Component.text("Tick monitor started. Before the monitor becomes fully active, the server's " + + sendMessage(text("Tick monitor started. Before the monitor becomes fully active, the server's " + "average tick rate will be calculated over a period of 120 ticks (approx 6 seconds).")); return; } @@ -95,63 +131,63 @@ public abstract class TickMonitor implements TickHook.Callback, GarbageCollectio } // form averages - if (this.state == State.SETUP) { - this.averageTickTime.accept(tickDuration); + if (this.phase == Phase.SETUP) { + this.averageTickTimeCalc.accept(tickDuration); // move onto the next state - if (this.averageTickTime.getCount() >= 120) { + if (this.averageTickTimeCalc.getCount() >= 120) { this.platform.getPlugin().executeAsync(() -> { - sendMessage(Component.text("Analysis is now complete.", NamedTextColor.GOLD)); - sendMessage(Component.text() - .color(NamedTextColor.GRAY) - .append(Component.text(">", NamedTextColor.WHITE)) - .append(Component.space()) - .append(Component.text("Max: ")) - .append(Component.text(df.format(this.averageTickTime.getMax()))) - .append(Component.text("ms")) + sendMessage(text("Analysis is now complete.", GOLD)); + sendMessage(text() + .color(GRAY) + .append(text(">", WHITE)) + .append(space()) + .append(text("Max: ")) + .append(text(DF.format(this.averageTickTimeCalc.getMax()))) + .append(text("ms")) .build() ); - sendMessage(Component.text() - .color(NamedTextColor.GRAY) - .append(Component.text(">", NamedTextColor.WHITE)) - .append(Component.space()) - .append(Component.text("Min: ")) - .append(Component.text(df.format(this.averageTickTime.getMin()))) - .append(Component.text("ms")) + sendMessage(text() + .color(GRAY) + .append(text(">", WHITE)) + .append(space()) + .append(text("Min: ")) + .append(text(DF.format(this.averageTickTimeCalc.getMin()))) + .append(text("ms")) .build() ); - sendMessage(Component.text() - .color(NamedTextColor.GRAY) - .append(Component.text(">", NamedTextColor.WHITE)) - .append(Component.space()) - .append(Component.text("Average: ")) - .append(Component.text(df.format(this.averageTickTime.getAverage()))) - .append(Component.text("ms")) + sendMessage(text() + .color(GRAY) + .append(text(">", WHITE)) + .append(space()) + .append(text("Average: ")) + .append(text(DF.format(this.averageTickTimeCalc.getAverage()))) + .append(text("ms")) .build() ); sendMessage(this.reportPredicate.monitoringStartMessage()); }); - this.avg = this.averageTickTime.getAverage(); - this.state = State.MONITORING; + this.averageTickTime = this.averageTickTimeCalc.getAverage(); + this.phase = Phase.MONITORING; } } - if (this.state == State.MONITORING) { - double increase = tickDuration - this.avg; - double percentageChange = (increase * 100d) / this.avg; + if (this.phase == Phase.MONITORING) { + double increase = tickDuration - this.averageTickTime; + double percentageChange = (increase * 100d) / this.averageTickTime; if (this.reportPredicate.shouldReport(tickDuration, increase, percentageChange)) { this.platform.getPlugin().executeAsync(() -> { - sendMessage(Component.text() - .color(NamedTextColor.GRAY) - .append(Component.text("Tick ")) - .append(Component.text("#" + getCurrentTick(), NamedTextColor.DARK_GRAY)) - .append(Component.text(" lasted ")) - .append(Component.text(df.format(tickDuration), NamedTextColor.GOLD)) - .append(Component.text(" ms. ")) - .append(Component.text("(")) - .append(Component.text(df.format(percentageChange) + "%", NamedTextColor.GOLD)) - .append(Component.text(" increase from avg)")) + sendMessage(text() + .color(GRAY) + .append(text("Tick ")) + .append(text("#" + getCurrentTick(), DARK_GRAY)) + .append(text(" lasted ")) + .append(text(DF.format(tickDuration), GOLD)) + .append(text(" ms. ")) + .append(text("(")) + .append(text(DF.format(percentageChange) + "%", GOLD)) + .append(text(" increase from avg)")) .build() ); }); @@ -161,105 +197,25 @@ public abstract class TickMonitor implements TickHook.Callback, GarbageCollectio @Override public void onGc(GarbageCollectionNotificationInfo data) { - if (this.state == State.SETUP) { + if (this.phase == Phase.SETUP) { // set lastTickTime to zero so this tick won't be counted in the average this.lastTickTime = 0; return; } - String gcType; - if (data.getGcAction().equals("end of minor GC")) { - gcType = "Young Gen"; - } else if (data.getGcAction().equals("end of major GC")) { - gcType = "Old Gen"; - } else { - gcType = data.getGcAction(); - } - this.platform.getPlugin().executeAsync(() -> { - sendMessage(Component.text() - .color(NamedTextColor.GRAY) - .append(Component.text("Tick ")) - .append(Component.text("#" + getCurrentTick(), NamedTextColor.DARK_GRAY)) - .append(Component.text(" included ")) - .append(Component.text("GC", NamedTextColor.RED)) - .append(Component.text(" lasting ")) - .append(Component.text(df.format(data.getGcInfo().getDuration()), NamedTextColor.GOLD)) - .append(Component.text(" ms. (type = " + gcType + ")")) + sendMessage(text() + .color(GRAY) + .append(text("Tick ")) + .append(text("#" + getCurrentTick(), DARK_GRAY)) + .append(text(" included ")) + .append(text("GC", RED)) + .append(text(" lasting ")) + .append(text(DF.format(data.getGcInfo().getDuration()), GOLD)) + .append(text(" ms. (type = " + GarbageCollectionMonitor.getGcType(data) + ")")) .build() ); }); } - /** - * A predicate to test whether a tick should be reported. - */ - public interface ReportPredicate { - - /** - * Tests whether a tick should be reported. - * - * @param duration the tick duration - * @param increaseFromAvg the difference between the ticks duration and the average - * @param percentageChange the percentage change between the ticks duration and the average - * @return true if the tick should be reported, false otherwise - */ - boolean shouldReport(double duration, double increaseFromAvg, double percentageChange); - - /** - * Gets a component to describe how the predicate will select ticks to report. - * - * @return the component - */ - Component monitoringStartMessage(); - - final class PercentageChangeGt implements ReportPredicate { - private final double threshold; - - public PercentageChangeGt(double threshold) { - this.threshold = threshold; - } - - @Override - public boolean shouldReport(double duration, double increaseFromAvg, double percentageChange) { - if (increaseFromAvg <= 0) { - return false; - } - return percentageChange > this.threshold; - } - - @Override - public Component monitoringStartMessage() { - return Component.text("Starting now, any ticks with >" + this.threshold + "% increase in " + - "duration compared to the average will be reported."); - } - } - - final class DurationGt implements ReportPredicate { - private final double threshold; - - public DurationGt(double threshold) { - this.threshold = threshold; - } - - @Override - public boolean shouldReport(double duration, double increaseFromAvg, double percentageChange) { - if (increaseFromAvg <= 0) { - return false; - } - return duration > this.threshold; - } - - @Override - public Component monitoringStartMessage() { - return Component.text("Starting now, any ticks with duration >" + this.threshold + " will be reported."); - } - } - - } - - private enum State { - SETUP, - MONITORING - } } diff --git a/spark-common/src/main/java/me/lucko/spark/common/monitor/tick/TickStatistics.java b/spark-common/src/main/java/me/lucko/spark/common/monitor/tick/TickStatistics.java index 9e6f7c5..31b58e9 100644 --- a/spark-common/src/main/java/me/lucko/spark/common/monitor/tick/TickStatistics.java +++ b/spark-common/src/main/java/me/lucko/spark/common/monitor/tick/TickStatistics.java @@ -20,8 +20,8 @@ package me.lucko.spark.common.monitor.tick; -import me.lucko.spark.common.sampler.tick.TickHook; -import me.lucko.spark.common.sampler.tick.TickReporter; +import me.lucko.spark.common.tick.TickHook; +import me.lucko.spark.common.tick.TickReporter; import me.lucko.spark.common.util.RollingAverage; import java.math.BigDecimal; diff --git a/spark-common/src/main/java/me/lucko/spark/common/sampler/SamplerBuilder.java b/spark-common/src/main/java/me/lucko/spark/common/sampler/SamplerBuilder.java index 7abe1a7..7dff29e 100644 --- a/spark-common/src/main/java/me/lucko/spark/common/sampler/SamplerBuilder.java +++ b/spark-common/src/main/java/me/lucko/spark/common/sampler/SamplerBuilder.java @@ -23,7 +23,7 @@ package me.lucko.spark.common.sampler; import me.lucko.spark.common.sampler.async.AsyncProfilerAccess; import me.lucko.spark.common.sampler.async.AsyncSampler; import me.lucko.spark.common.sampler.java.JavaSampler; -import me.lucko.spark.common.sampler.tick.TickHook; +import me.lucko.spark.common.tick.TickHook; import java.util.concurrent.TimeUnit; diff --git a/spark-common/src/main/java/me/lucko/spark/common/sampler/aggregator/DataAggregator.java b/spark-common/src/main/java/me/lucko/spark/common/sampler/aggregator/DataAggregator.java index a91a998..8b90639 100644 --- a/spark-common/src/main/java/me/lucko/spark/common/sampler/aggregator/DataAggregator.java +++ b/spark-common/src/main/java/me/lucko/spark/common/sampler/aggregator/DataAggregator.java @@ -23,7 +23,6 @@ package me.lucko.spark.common.sampler.aggregator; import me.lucko.spark.common.sampler.node.ThreadNode; import me.lucko.spark.proto.SparkProtos.SamplerMetadata; -import java.lang.management.ThreadInfo; import java.util.Map; /** diff --git a/spark-common/src/main/java/me/lucko/spark/common/sampler/async/AsyncSampler.java b/spark-common/src/main/java/me/lucko/spark/common/sampler/async/AsyncSampler.java index a109be7..1e23124 100644 --- a/spark-common/src/main/java/me/lucko/spark/common/sampler/async/AsyncSampler.java +++ b/spark-common/src/main/java/me/lucko/spark/common/sampler/async/AsyncSampler.java @@ -25,11 +25,7 @@ import me.lucko.spark.common.platform.PlatformInfo; import me.lucko.spark.common.sampler.Sampler; import me.lucko.spark.common.sampler.ThreadDumper; import me.lucko.spark.common.sampler.ThreadGrouper; -import me.lucko.spark.common.sampler.async.jfr.ClassRef; import me.lucko.spark.common.sampler.async.jfr.JfrReader; -import me.lucko.spark.common.sampler.async.jfr.MethodRef; -import me.lucko.spark.common.sampler.async.jfr.Sample; -import me.lucko.spark.common.sampler.async.jfr.StackTrace; import me.lucko.spark.common.sampler.node.MergeMode; import me.lucko.spark.common.sampler.node.ThreadNode; import me.lucko.spark.proto.SparkProtos; @@ -206,9 +202,9 @@ public class AsyncSampler implements Sampler { } private void readSegments(JfrReader reader, Predicate<String> threadFilter) { - List<Sample> samples = reader.samples; + List<JfrReader.Sample> samples = reader.samples; for (int i = 0; i < samples.size(); i++) { - Sample sample = samples.get(i); + JfrReader.Sample sample = samples.get(i); long duration; if (i == 0) { @@ -232,8 +228,8 @@ public class AsyncSampler implements Sampler { } } - private static ProfileSegment parseSegment(JfrReader reader, Sample sample, String threadName, long duration) { - StackTrace stackTrace = reader.stackTraces.get(sample.stackTraceId); + private static ProfileSegment parseSegment(JfrReader reader, JfrReader.Sample sample, String threadName, long duration) { + JfrReader.StackTrace stackTrace = reader.stackTraces.get(sample.stackTraceId); int len = stackTrace.methods.length; AsyncStackTraceElement[] stack = new AsyncStackTraceElement[len]; @@ -250,8 +246,8 @@ public class AsyncSampler implements Sampler { return result; } - MethodRef methodRef = reader.methods.get(methodId); - ClassRef classRef = reader.classes.get(methodRef.cls); + JfrReader.MethodRef methodRef = reader.methods.get(methodId); + JfrReader.ClassRef classRef = reader.classes.get(methodRef.cls); byte[] className = reader.symbols.get(classRef.name); byte[] methodName = reader.symbols.get(methodRef.name); diff --git a/spark-common/src/main/java/me/lucko/spark/common/sampler/async/jfr/ClassRef.java b/spark-common/src/main/java/me/lucko/spark/common/sampler/async/jfr/ClassRef.java deleted file mode 100644 index 2366fa6..0000000 --- a/spark-common/src/main/java/me/lucko/spark/common/sampler/async/jfr/ClassRef.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright 2020 Andrei Pangin - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package me.lucko.spark.common.sampler.async.jfr; - -public class ClassRef { - public final long name; - - public ClassRef(long name) { - this.name = name; - } -} diff --git a/spark-common/src/main/java/me/lucko/spark/common/sampler/async/jfr/Element.java b/spark-common/src/main/java/me/lucko/spark/common/sampler/async/jfr/Element.java deleted file mode 100644 index 9d6b6c7..0000000 --- a/spark-common/src/main/java/me/lucko/spark/common/sampler/async/jfr/Element.java +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright 2020 Andrei Pangin - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package me.lucko.spark.common.sampler.async.jfr; - -class Element { - - void addChild(Element e) { - } -} diff --git a/spark-common/src/main/java/me/lucko/spark/common/sampler/async/jfr/JfrClass.java b/spark-common/src/main/java/me/lucko/spark/common/sampler/async/jfr/JfrClass.java deleted file mode 100644 index a171552..0000000 --- a/spark-common/src/main/java/me/lucko/spark/common/sampler/async/jfr/JfrClass.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright 2020 Andrei Pangin - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package me.lucko.spark.common.sampler.async.jfr; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -class JfrClass extends Element { - final int id; - final String name; - final List<JfrField> fields; - - JfrClass(Map<String, String> attributes) { - this.id = Integer.parseInt(attributes.get("id")); - this.name = attributes.get("name"); - this.fields = new ArrayList<>(2); - } - - @Override - void addChild(Element e) { - if (e instanceof JfrField) { - fields.add((JfrField) e); - } - } - - JfrField field(String name) { - for (JfrField field : fields) { - if (field.name.equals(name)) { - return field; - } - } - return null; - } -} diff --git a/spark-common/src/main/java/me/lucko/spark/common/sampler/async/jfr/JfrField.java b/spark-common/src/main/java/me/lucko/spark/common/sampler/async/jfr/JfrField.java deleted file mode 100644 index 7a78f2c..0000000 --- a/spark-common/src/main/java/me/lucko/spark/common/sampler/async/jfr/JfrField.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright 2020 Andrei Pangin - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package me.lucko.spark.common.sampler.async.jfr; - -import java.util.Map; - -class JfrField extends Element { - final String name; - final int type; - final boolean constantPool; - - JfrField(Map<String, String> attributes) { - this.name = attributes.get("name"); - this.type = Integer.parseInt(attributes.get("class")); - this.constantPool = "true".equals(attributes.get("constantPool")); - } -} diff --git a/spark-common/src/main/java/me/lucko/spark/common/sampler/async/jfr/JfrReader.java b/spark-common/src/main/java/me/lucko/spark/common/sampler/async/jfr/JfrReader.java index 49a3474..95c9bad 100644 --- a/spark-common/src/main/java/me/lucko/spark/common/sampler/async/jfr/JfrReader.java +++ b/spark-common/src/main/java/me/lucko/spark/common/sampler/async/jfr/JfrReader.java @@ -24,7 +24,6 @@ import java.nio.ByteBuffer; import java.nio.channels.FileChannel; import java.nio.charset.StandardCharsets; import java.nio.file.Path; -import java.nio.file.Paths; import java.nio.file.StandardOpenOption; import java.util.ArrayList; import java.util.Collections; @@ -386,4 +385,100 @@ public class JfrReader implements Closeable { buf.get(bytes); return bytes; } + + public static class ClassRef { + public final long name; + + public ClassRef(long name) { + this.name = name; + } + } + + static class Element { + + void addChild(Element e) { + } + } + + static class JfrClass extends Element { + final int id; + final String name; + final List<JfrField> fields; + + JfrClass(Map<String, String> attributes) { + this.id = Integer.parseInt(attributes.get("id")); + this.name = attributes.get("name"); + this.fields = new ArrayList<>(2); + } + + @Override + void addChild(Element e) { + if (e instanceof JfrField) { + fields.add((JfrField) e); + } + } + + JfrField field(String name) { + for (JfrField field : fields) { + if (field.name.equals(name)) { + return field; + } + } + return null; + } + } + + static class JfrField extends Element { + final String name; + final int type; + final boolean constantPool; + + JfrField(Map<String, String> attributes) { + this.name = attributes.get("name"); + this.type = Integer.parseInt(attributes.get("class")); + this.constantPool = "true".equals(attributes.get("constantPool")); + } + } + + public static class MethodRef { + public final long cls; + public final long name; + public final long sig; + + public MethodRef(long cls, long name, long sig) { + this.cls = cls; + this.name = name; + this.sig = sig; + } + } + + public static class Sample implements Comparable<Sample> { + public final long time; + public final int tid; + public final int stackTraceId; + public final int threadState; + + public Sample(long time, int tid, int stackTraceId, int threadState) { + this.time = time; + this.tid = tid; + this.stackTraceId = stackTraceId; + this.threadState = threadState; + } + + @Override + public int compareTo(Sample o) { + return Long.compare(time, o.time); + } + } + + public static class StackTrace { + public final long[] methods; + public final byte[] types; + public long samples; + + public StackTrace(long[] methods, byte[] types) { + this.methods = methods; + this.types = types; + } + } } diff --git a/spark-common/src/main/java/me/lucko/spark/common/sampler/async/jfr/MethodRef.java b/spark-common/src/main/java/me/lucko/spark/common/sampler/async/jfr/MethodRef.java deleted file mode 100644 index 2f9071e..0000000 --- a/spark-common/src/main/java/me/lucko/spark/common/sampler/async/jfr/MethodRef.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright 2020 Andrei Pangin - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package me.lucko.spark.common.sampler.async.jfr; - -public class MethodRef { - public final long cls; - public final long name; - public final long sig; - - public MethodRef(long cls, long name, long sig) { - this.cls = cls; - this.name = name; - this.sig = sig; - } -} diff --git a/spark-common/src/main/java/me/lucko/spark/common/sampler/async/jfr/Sample.java b/spark-common/src/main/java/me/lucko/spark/common/sampler/async/jfr/Sample.java deleted file mode 100644 index 095e261..0000000 --- a/spark-common/src/main/java/me/lucko/spark/common/sampler/async/jfr/Sample.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright 2020 Andrei Pangin - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package me.lucko.spark.common.sampler.async.jfr; - -public class Sample implements Comparable<Sample> { - public final long time; - public final int tid; - public final int stackTraceId; - public final int threadState; - - public Sample(long time, int tid, int stackTraceId, int threadState) { - this.time = time; - this.tid = tid; - this.stackTraceId = stackTraceId; - this.threadState = threadState; - } - - @Override - public int compareTo(Sample o) { - return Long.compare(time, o.time); - } -} diff --git a/spark-common/src/main/java/me/lucko/spark/common/sampler/async/jfr/StackTrace.java b/spark-common/src/main/java/me/lucko/spark/common/sampler/async/jfr/StackTrace.java deleted file mode 100644 index 4c12c5e..0000000 --- a/spark-common/src/main/java/me/lucko/spark/common/sampler/async/jfr/StackTrace.java +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright 2020 Andrei Pangin - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package me.lucko.spark.common.sampler.async.jfr; - -public class StackTrace { - public final long[] methods; - public final byte[] types; - public long samples; - - public StackTrace(long[] methods, byte[] types) { - this.methods = methods; - this.types = types; - } -} diff --git a/spark-common/src/main/java/me/lucko/spark/common/sampler/java/JavaSampler.java b/spark-common/src/main/java/me/lucko/spark/common/sampler/java/JavaSampler.java index 568609e..0ee5f86 100644 --- a/spark-common/src/main/java/me/lucko/spark/common/sampler/java/JavaSampler.java +++ b/spark-common/src/main/java/me/lucko/spark/common/sampler/java/JavaSampler.java @@ -22,6 +22,7 @@ package me.lucko.spark.common.sampler.java; import com.google.common.util.concurrent.ThreadFactoryBuilder; + import me.lucko.spark.common.command.sender.CommandSender; import me.lucko.spark.common.platform.PlatformInfo; import me.lucko.spark.common.sampler.Sampler; @@ -29,7 +30,7 @@ import me.lucko.spark.common.sampler.ThreadDumper; import me.lucko.spark.common.sampler.ThreadGrouper; import me.lucko.spark.common.sampler.node.MergeMode; import me.lucko.spark.common.sampler.node.ThreadNode; -import me.lucko.spark.common.sampler.tick.TickHook; +import me.lucko.spark.common.tick.TickHook; import me.lucko.spark.proto.SparkProtos.SamplerData; import me.lucko.spark.proto.SparkProtos.SamplerMetadata; diff --git a/spark-common/src/main/java/me/lucko/spark/common/sampler/java/TickedDataAggregator.java b/spark-common/src/main/java/me/lucko/spark/common/sampler/java/TickedDataAggregator.java index 1a0bcdd..018a3b8 100644 --- a/spark-common/src/main/java/me/lucko/spark/common/sampler/java/TickedDataAggregator.java +++ b/spark-common/src/main/java/me/lucko/spark/common/sampler/java/TickedDataAggregator.java @@ -23,7 +23,7 @@ package me.lucko.spark.common.sampler.java; import me.lucko.spark.common.sampler.ThreadGrouper; import me.lucko.spark.common.sampler.aggregator.DataAggregator; import me.lucko.spark.common.sampler.node.ThreadNode; -import me.lucko.spark.common.sampler.tick.TickHook; +import me.lucko.spark.common.tick.TickHook; import me.lucko.spark.proto.SparkProtos.SamplerMetadata; import java.lang.management.ThreadInfo; diff --git a/spark-common/src/main/java/me/lucko/spark/common/sampler/node/StackTraceNode.java b/spark-common/src/main/java/me/lucko/spark/common/sampler/node/StackTraceNode.java index bd731c1..4179464 100644 --- a/spark-common/src/main/java/me/lucko/spark/common/sampler/node/StackTraceNode.java +++ b/spark-common/src/main/java/me/lucko/spark/common/sampler/node/StackTraceNode.java @@ -24,7 +24,6 @@ package me.lucko.spark.common.sampler.node; import me.lucko.spark.common.util.MethodDisambiguator; import me.lucko.spark.proto.SparkProtos; -import java.util.Comparator; import java.util.Objects; /** diff --git a/spark-common/src/main/java/me/lucko/spark/common/sampler/tick/AbstractTickHook.java b/spark-common/src/main/java/me/lucko/spark/common/tick/AbstractTickHook.java index 72e927f..a6e8745 100644 --- a/spark-common/src/main/java/me/lucko/spark/common/sampler/tick/AbstractTickHook.java +++ b/spark-common/src/main/java/me/lucko/spark/common/tick/AbstractTickHook.java @@ -18,7 +18,7 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package me.lucko.spark.common.sampler.tick; +package me.lucko.spark.common.tick; import java.util.HashSet; import java.util.Set; diff --git a/spark-common/src/main/java/me/lucko/spark/common/sampler/tick/AbstractTickReporter.java b/spark-common/src/main/java/me/lucko/spark/common/tick/AbstractTickReporter.java index 4005f87..74a814d 100644 --- a/spark-common/src/main/java/me/lucko/spark/common/sampler/tick/AbstractTickReporter.java +++ b/spark-common/src/main/java/me/lucko/spark/common/tick/AbstractTickReporter.java @@ -18,7 +18,7 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package me.lucko.spark.common.sampler.tick; +package me.lucko.spark.common.tick; import java.util.HashSet; import java.util.Set; diff --git a/spark-common/src/main/java/me/lucko/spark/common/sampler/tick/TickHook.java b/spark-common/src/main/java/me/lucko/spark/common/tick/TickHook.java index 8731216..e673258 100644 --- a/spark-common/src/main/java/me/lucko/spark/common/sampler/tick/TickHook.java +++ b/spark-common/src/main/java/me/lucko/spark/common/tick/TickHook.java @@ -18,7 +18,7 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package me.lucko.spark.common.sampler.tick; +package me.lucko.spark.common.tick; /** * A hook with the game's "tick loop". diff --git a/spark-common/src/main/java/me/lucko/spark/common/sampler/tick/TickReporter.java b/spark-common/src/main/java/me/lucko/spark/common/tick/TickReporter.java index e922e72..b575f59 100644 --- a/spark-common/src/main/java/me/lucko/spark/common/sampler/tick/TickReporter.java +++ b/spark-common/src/main/java/me/lucko/spark/common/tick/TickReporter.java @@ -18,7 +18,7 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package me.lucko.spark.common.sampler.tick; +package me.lucko.spark.common.tick; /** * A reporting callback for the game's "tick loop". diff --git a/spark-common/src/main/java/me/lucko/spark/common/util/AbstractHttpClient.java b/spark-common/src/main/java/me/lucko/spark/common/util/AbstractHttpClient.java index 1ff169d..8ece3d4 100644 --- a/spark-common/src/main/java/me/lucko/spark/common/util/AbstractHttpClient.java +++ b/spark-common/src/main/java/me/lucko/spark/common/util/AbstractHttpClient.java @@ -38,6 +38,7 @@ public class AbstractHttpClient { protected Response makeHttpRequest(Request request) throws IOException { Response response = this.okHttp.newCall(request).execute(); if (!response.isSuccessful()) { + response.close(); throw new RuntimeException("Request was unsuccessful: " + response.code() + " - " + response.message()); } return response; diff --git a/spark-common/src/main/java/me/lucko/spark/common/util/BytebinClient.java b/spark-common/src/main/java/me/lucko/spark/common/util/BytebinClient.java index ff8f4e3..9202303 100644 --- a/spark-common/src/main/java/me/lucko/spark/common/util/BytebinClient.java +++ b/spark-common/src/main/java/me/lucko/spark/common/util/BytebinClient.java @@ -51,11 +51,7 @@ public class BytebinClient extends AbstractHttpClient { */ public BytebinClient(OkHttpClient okHttpClient, String url, String userAgent) { super(okHttpClient); - if (url.endsWith("/")) { - this.url = url; - } else { - this.url = url + "/"; - } + this.url = url + (url.endsWith("/") ? "" : "/"); this.userAgent = userAgent; } @@ -64,11 +60,10 @@ public class BytebinClient extends AbstractHttpClient { * * @param buf the compressed content * @param contentType the type of the content - * @param allowModification if the paste should be modifiable * @return the key of the resultant content * @throws IOException if an error occurs */ - public Content postContent(byte[] buf, MediaType contentType, boolean allowModification) throws IOException { + public Content postContent(byte[] buf, MediaType contentType) throws IOException { RequestBody body = RequestBody.create(contentType, buf); Request.Builder requestBuilder = new Request.Builder() @@ -76,69 +71,21 @@ public class BytebinClient extends AbstractHttpClient { .header("User-Agent", this.userAgent) .header("Content-Encoding", "gzip"); - if (allowModification) { - requestBuilder.header("Allow-Modification", "true"); - } - Request request = requestBuilder.post(body).build(); try (Response response = makeHttpRequest(request)) { String key = response.header("Location"); if (key == null) { throw new IllegalStateException("Key not returned"); } - - if (allowModification) { - String modificationKey = response.header("Modification-Key"); - if (modificationKey == null) { - throw new IllegalStateException("Modification key not returned"); - } - return new Content(key, modificationKey); - } else { - return new Content(key); - } - } - } - - /** - * PUTs modified GZIP compressed content to bytebin in place of existing content. - * - * @param existingContent the existing content - * @param buf the compressed content to put - * @param contentType the type of the content - * @throws IOException if an error occurs - */ - public void modifyContent(Content existingContent, byte[] buf, MediaType contentType) throws IOException { - if (!existingContent.modifiable) { - throw new IllegalArgumentException("Existing content is not modifiable"); + return new Content(key); } - - RequestBody body = RequestBody.create(contentType, buf); - - Request.Builder requestBuilder = new Request.Builder() - .url(this.url + existingContent.key()) - .header("User-Agent", this.userAgent) - .header("Content-Encoding", "gzip") - .header("Modification-Key", existingContent.modificationKey); - - Request request = requestBuilder.put(body).build(); - makeHttpRequest(request).close(); } public static final class Content { private final String key; - private final boolean modifiable; - private final String modificationKey; Content(String key) { this.key = key; - this.modifiable = false; - this.modificationKey = null; - } - - Content(String key, String modificationKey) { - this.key = key; - this.modifiable = true; - this.modificationKey = modificationKey; } public String key() { diff --git a/spark-common/src/main/java/me/lucko/spark/common/util/MethodDisambiguator.java b/spark-common/src/main/java/me/lucko/spark/common/util/MethodDisambiguator.java index e6311e0..2e113a9 100644 --- a/spark-common/src/main/java/me/lucko/spark/common/util/MethodDisambiguator.java +++ b/spark-common/src/main/java/me/lucko/spark/common/util/MethodDisambiguator.java @@ -23,7 +23,9 @@ package me.lucko.spark.common.util; import com.google.common.collect.ImmutableListMultimap; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ListMultimap; + import me.lucko.spark.common.sampler.node.StackTraceNode; + import org.objectweb.asm.ClassReader; import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.Label; diff --git a/spark-common/src/main/java/me/lucko/spark/common/util/RollingAverage.java b/spark-common/src/main/java/me/lucko/spark/common/util/RollingAverage.java index 8160e96..2c6219a 100644 --- a/spark-common/src/main/java/me/lucko/spark/common/util/RollingAverage.java +++ b/spark-common/src/main/java/me/lucko/spark/common/util/RollingAverage.java @@ -23,9 +23,7 @@ package me.lucko.spark.common.util; import java.math.BigDecimal; import java.math.RoundingMode; import java.util.ArrayDeque; -import java.util.ArrayList; import java.util.Arrays; -import java.util.List; import java.util.Queue; public class RollingAverage { diff --git a/spark-common/src/main/java/me/lucko/spark/common/util/ThreadFinder.java b/spark-common/src/main/java/me/lucko/spark/common/util/ThreadFinder.java index 0d1cbd3..66aa941 100644 --- a/spark-common/src/main/java/me/lucko/spark/common/util/ThreadFinder.java +++ b/spark-common/src/main/java/me/lucko/spark/common/util/ThreadFinder.java @@ -21,7 +21,6 @@ package me.lucko.spark.common.util; import java.util.Arrays; -import java.util.Objects; import java.util.stream.Stream; /** @@ -56,7 +55,7 @@ public final class ThreadFinder { threads = new Thread[threads.length * 2]; } this.approxActiveCount = len; - return Arrays.stream(threads).filter(Objects::nonNull); + return Arrays.stream(threads, 0, len); } } |