aboutsummaryrefslogtreecommitdiff
path: root/spark-common/src/main/java
diff options
context:
space:
mode:
authorlucko <git@lucko.me>2022-11-13 22:05:02 +0000
committerGitHub <noreply@github.com>2022-11-13 22:05:02 +0000
commitab458490f4304ae8b8b707f55765690591c2dbe3 (patch)
tree1ceef4324fc9ece8f9c4389e2f4da07cb844c3d2 /spark-common/src/main/java
parent5af2e6fb4cbd21f836c7ad56100b3c4535a831de (diff)
parentf2d77d875f32f107987c93da1f90529fc6812444 (diff)
downloadspark-ab458490f4304ae8b8b707f55765690591c2dbe3.tar.gz
spark-ab458490f4304ae8b8b707f55765690591c2dbe3.tar.bz2
spark-ab458490f4304ae8b8b707f55765690591c2dbe3.zip
Merge pull request #265 from lucko/background
Diffstat (limited to 'spark-common/src/main/java')
-rw-r--r--spark-common/src/main/java/me/lucko/spark/common/SparkPlatform.java88
-rw-r--r--spark-common/src/main/java/me/lucko/spark/common/command/Arguments.java11
-rw-r--r--spark-common/src/main/java/me/lucko/spark/common/command/Command.java58
-rw-r--r--spark-common/src/main/java/me/lucko/spark/common/command/modules/GcMonitoringModule.java22
-rw-r--r--spark-common/src/main/java/me/lucko/spark/common/command/modules/SamplerModule.java163
-rw-r--r--spark-common/src/main/java/me/lucko/spark/common/sampler/AbstractSampler.java19
-rw-r--r--spark-common/src/main/java/me/lucko/spark/common/sampler/Sampler.java7
-rw-r--r--spark-common/src/main/java/me/lucko/spark/common/sampler/SamplerBuilder.java29
-rw-r--r--spark-common/src/main/java/me/lucko/spark/common/sampler/SamplerContainer.java85
-rw-r--r--spark-common/src/main/java/me/lucko/spark/common/sampler/SamplerSettings.java61
-rw-r--r--spark-common/src/main/java/me/lucko/spark/common/sampler/aggregator/AbstractDataAggregator.java6
-rw-r--r--spark-common/src/main/java/me/lucko/spark/common/sampler/aggregator/DataAggregator.java8
-rw-r--r--spark-common/src/main/java/me/lucko/spark/common/sampler/async/AsyncSampler.java26
-rw-r--r--spark-common/src/main/java/me/lucko/spark/common/sampler/java/JavaSampler.java27
-rw-r--r--spark-common/src/main/java/me/lucko/spark/common/sampler/node/AbstractNode.java28
-rw-r--r--spark-common/src/main/java/me/lucko/spark/common/sampler/node/ThreadNode.java45
-rw-r--r--spark-common/src/main/java/me/lucko/spark/common/sampler/window/ProfilingWindowUtils.java38
-rw-r--r--spark-common/src/main/java/me/lucko/spark/common/sampler/window/ProtoTimeEncoder.java7
-rw-r--r--spark-common/src/main/java/me/lucko/spark/common/sampler/window/WindowStatisticsCollector.java5
-rw-r--r--spark-common/src/main/java/me/lucko/spark/common/util/Configuration.java10
-rw-r--r--spark-common/src/main/java/me/lucko/spark/common/util/FormatUtil.java20
21 files changed, 601 insertions, 162 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 4c3875c..5461ed4 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
@@ -45,6 +45,10 @@ import me.lucko.spark.common.monitor.ping.PingStatistics;
import me.lucko.spark.common.monitor.ping.PlayerPingProvider;
import me.lucko.spark.common.monitor.tick.TickStatistics;
import me.lucko.spark.common.platform.PlatformStatisticsProvider;
+import me.lucko.spark.common.sampler.Sampler;
+import me.lucko.spark.common.sampler.SamplerBuilder;
+import me.lucko.spark.common.sampler.SamplerContainer;
+import me.lucko.spark.common.sampler.ThreadGrouper;
import me.lucko.spark.common.sampler.source.ClassSourceLookup;
import me.lucko.spark.common.tick.TickHook;
import me.lucko.spark.common.tick.TickReporter;
@@ -63,6 +67,7 @@ import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
+import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -73,7 +78,6 @@ import java.util.stream.Collectors;
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;
@@ -98,6 +102,7 @@ public class SparkPlatform {
private final List<Command> commands;
private final ReentrantLock commandExecuteLock = new ReentrantLock(true);
private final ActivityLog activityLog;
+ private final SamplerContainer samplerContainer;
private final TickHook tickHook;
private final TickReporter tickReporter;
private final TickStatistics tickStatistics;
@@ -137,6 +142,8 @@ public class SparkPlatform {
this.activityLog = new ActivityLog(plugin.getPluginDirectory().resolve("activity.json"));
this.activityLog.load();
+ this.samplerContainer = new SamplerContainer(this.configuration.getBoolean("backgroundProfiler", true));
+
this.tickHook = plugin.createTickHook();
this.tickReporter = plugin.createTickReporter();
this.tickStatistics = this.tickHook != null || this.tickReporter != null ? new TickStatistics() : null;
@@ -175,6 +182,16 @@ public class SparkPlatform {
SparkApi api = new SparkApi(this);
this.plugin.registerApi(api);
SparkApi.register(api);
+
+ if (this.samplerContainer.isBackgroundProfilerEnabled()) {
+ this.plugin.log(Level.INFO, "Starting background profiler...");
+ try {
+ startBackgroundProfiler();
+ this.plugin.log(Level.INFO, "... done!");
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
}
public void disable() {
@@ -192,6 +209,8 @@ public class SparkPlatform {
module.close();
}
+ this.samplerContainer.close();
+
SparkApi.unregister();
this.temporaryFiles.deleteTemporaryFiles();
@@ -229,6 +248,10 @@ public class SparkPlatform {
return this.activityLog;
}
+ public SamplerContainer getSamplerContainer() {
+ return this.samplerContainer;
+ }
+
public TickHook getTickHook() {
return this.tickHook;
}
@@ -261,6 +284,17 @@ public class SparkPlatform {
return this.serverNormalOperationStartTime;
}
+ public void startBackgroundProfiler() {
+ Sampler sampler = new SamplerBuilder()
+ .background(true)
+ .threadDumper(this.plugin.getDefaultThreadDumper())
+ .threadGrouper(ThreadGrouper.BY_POOL)
+ .samplingInterval(this.configuration.getInteger("backgroundProfilerInterval", 10))
+ .start(this);
+
+ this.samplerContainer.setActiveSampler(sampler);
+ }
+
public Path resolveSaveFile(String prefix, String extension) {
Path pluginFolder = this.plugin.getPluginDirectory();
try {
@@ -386,7 +420,7 @@ public class SparkPlatform {
if (command.aliases().contains(alias)) {
resp.setCommandPrimaryAlias(command.primaryAlias());
try {
- command.executor().execute(this, sender, resp, new Arguments(rawArgs));
+ command.executor().execute(this, sender, resp, new Arguments(rawArgs, command.allowSubCommand()));
} catch (Arguments.ParseException e) {
resp.replyPrefixed(text(e.getMessage(), RED));
}
@@ -434,32 +468,38 @@ public class SparkPlatform {
);
for (Command command : commands) {
String usage = "/" + getPlugin().getCommandName() + " " + command.primaryAlias();
- ClickEvent clickEvent = ClickEvent.suggestCommand(usage);
- sender.reply(text()
- .append(text(">", GOLD, BOLD))
- .append(space())
- .append(text().content(usage).color(GRAY).clickEvent(clickEvent).build())
- .build()
- );
- for (Command.ArgumentInfo arg : command.arguments()) {
- if (arg.requiresParameter()) {
+
+ if (command.allowSubCommand()) {
+ Map<String, List<Command.ArgumentInfo>> argumentsBySubCommand = command.arguments().stream()
+ .collect(Collectors.groupingBy(Command.ArgumentInfo::subCommandName, LinkedHashMap::new, Collectors.toList()));
+
+ argumentsBySubCommand.forEach((subCommand, arguments) -> {
+ String subCommandUsage = usage + " " + subCommand;
+
sender.reply(text()
- .content(" ")
- .append(text("[", DARK_GRAY))
- .append(text("--" + arg.argumentName(), GRAY))
+ .append(text(">", GOLD, BOLD))
.append(space())
- .append(text("<" + arg.parameterDescription() + ">", DARK_GRAY))
- .append(text("]", DARK_GRAY))
- .build()
- );
- } else {
- sender.reply(text()
- .content(" ")
- .append(text("[", DARK_GRAY))
- .append(text("--" + arg.argumentName(), GRAY))
- .append(text("]", DARK_GRAY))
+ .append(text().content(subCommandUsage).color(GRAY).clickEvent(ClickEvent.suggestCommand(subCommandUsage)).build())
.build()
);
+
+ for (Command.ArgumentInfo arg : arguments) {
+ if (arg.argumentName().isEmpty()) {
+ continue;
+ }
+ sender.reply(arg.toComponent(" "));
+ }
+ });
+ } else {
+ sender.reply(text()
+ .append(text(">", GOLD, BOLD))
+ .append(space())
+ .append(text().content(usage).color(GRAY).clickEvent(ClickEvent.suggestCommand(usage)).build())
+ .build()
+ );
+
+ for (Command.ArgumentInfo arg : command.arguments()) {
+ sender.reply(arg.toComponent(" "));
}
}
}
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 17c49e2..ad8c777 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
@@ -38,8 +38,9 @@ public class Arguments {
private final List<String> rawArgs;
private final SetMultimap<String, String> parsedArgs;
+ private String parsedSubCommand = null;
- public Arguments(List<String> rawArgs) {
+ public Arguments(List<String> rawArgs, boolean allowSubCommand) {
this.rawArgs = rawArgs;
this.parsedArgs = HashMultimap.create();
@@ -52,7 +53,9 @@ public class Arguments {
Matcher matcher = FLAG_REGEX.matcher(arg);
boolean matches = matcher.matches();
- if (flag == null || matches) {
+ if (i == 0 && allowSubCommand && !matches) {
+ this.parsedSubCommand = arg;
+ } else if (flag == null || matches) {
if (!matches) {
throw new ParseException("Expected flag at position " + i + " but got '" + arg + "' instead!");
}
@@ -80,6 +83,10 @@ public class Arguments {
return this.rawArgs;
}
+ public String subCommand() {
+ return this.parsedSubCommand;
+ }
+
public int intFlag(String key) {
Iterator<String> it = this.parsedArgs.get(key).iterator();
if (it.hasNext()) {
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 dad15e6..c6871a9 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
@@ -25,10 +25,17 @@ import com.google.common.collect.ImmutableList;
import me.lucko.spark.common.SparkPlatform;
import me.lucko.spark.common.command.sender.CommandSender;
+import net.kyori.adventure.text.Component;
+
import java.util.Collections;
import java.util.List;
import java.util.Objects;
+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.GRAY;
+
public class Command {
public static Builder builder() {
@@ -39,12 +46,14 @@ public class Command {
private final List<ArgumentInfo> arguments;
private final Executor executor;
private final TabCompleter tabCompleter;
+ private final boolean allowSubCommand;
- private Command(List<String> aliases, List<ArgumentInfo> arguments, Executor executor, TabCompleter tabCompleter) {
+ private Command(List<String> aliases, List<ArgumentInfo> arguments, Executor executor, TabCompleter tabCompleter, boolean allowSubCommand) {
this.aliases = aliases;
this.arguments = arguments;
this.executor = executor;
this.tabCompleter = tabCompleter;
+ this.allowSubCommand = allowSubCommand;
}
public List<String> aliases() {
@@ -67,11 +76,16 @@ public class Command {
return this.aliases.get(0);
}
+ public boolean allowSubCommand() {
+ return this.allowSubCommand;
+ }
+
public static final class Builder {
private final ImmutableList.Builder<String> aliases = ImmutableList.builder();
private final ImmutableList.Builder<ArgumentInfo> arguments = ImmutableList.builder();
private Executor executor = null;
private TabCompleter tabCompleter = null;
+ private boolean allowSubCommand = false;
Builder() {
@@ -82,8 +96,13 @@ public class Command {
return this;
}
+ public Builder argumentUsage(String subCommandName, String argumentName, String parameterDescription) {
+ this.arguments.add(new ArgumentInfo(subCommandName, argumentName, parameterDescription));
+ return this;
+ }
+
public Builder argumentUsage(String argumentName, String parameterDescription) {
- this.arguments.add(new ArgumentInfo(argumentName, parameterDescription));
+ this.arguments.add(new ArgumentInfo("", argumentName, parameterDescription));
return this;
}
@@ -97,6 +116,11 @@ public class Command {
return this;
}
+ public Builder allowSubCommand(boolean allowSubCommand) {
+ this.allowSubCommand = allowSubCommand;
+ return this;
+ }
+
public Command build() {
List<String> aliases = this.aliases.build();
if (aliases.isEmpty()) {
@@ -108,7 +132,7 @@ public class Command {
if (this.tabCompleter == null) {
this.tabCompleter = TabCompleter.empty();
}
- return new Command(aliases, this.arguments.build(), this.executor, this.tabCompleter);
+ return new Command(aliases, this.arguments.build(), this.executor, this.tabCompleter, this.allowSubCommand);
}
}
@@ -127,14 +151,20 @@ public class Command {
}
public static final class ArgumentInfo {
+ private final String subCommandName;
private final String argumentName;
private final String parameterDescription;
- public ArgumentInfo(String argumentName, String parameterDescription) {
+ public ArgumentInfo(String subCommandName, String argumentName, String parameterDescription) {
+ this.subCommandName = subCommandName;
this.argumentName = argumentName;
this.parameterDescription = parameterDescription;
}
+ public String subCommandName() {
+ return this.subCommandName;
+ }
+
public String argumentName() {
return this.argumentName;
}
@@ -146,6 +176,26 @@ public class Command {
public boolean requiresParameter() {
return this.parameterDescription != null;
}
+
+ public Component toComponent(String padding) {
+ if (requiresParameter()) {
+ return text()
+ .content(padding)
+ .append(text("[", DARK_GRAY))
+ .append(text("--" + argumentName(), GRAY))
+ .append(space())
+ .append(text("<" + parameterDescription() + ">", DARK_GRAY))
+ .append(text("]", DARK_GRAY))
+ .build();
+ } else {
+ return text()
+ .content(padding)
+ .append(text("[", DARK_GRAY))
+ .append(text("--" + argumentName(), GRAY))
+ .append(text("]", DARK_GRAY))
+ .build();
+ }
+ }
}
}
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 2ce83fd..a2da0a0 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
@@ -123,7 +123,7 @@ public class GcMonitoringModule implements CommandModule {
);
report.add(text()
.content(" ")
- .append(text(formatTime((long) averageFrequency), WHITE))
+ .append(text(FormatUtil.formatSeconds((long) averageFrequency / 1000), WHITE))
.append(text(" avg frequency", GRAY))
.build()
);
@@ -153,26 +153,6 @@ public class GcMonitoringModule implements CommandModule {
);
}
- private static String formatTime(long millis) {
- if (millis <= 0) {
- return "0s";
- }
-
- long second = millis / 1000;
- long minute = second / 60;
- second = second % 60;
-
- StringBuilder sb = new StringBuilder();
- if (minute != 0) {
- sb.append(minute).append("m ");
- }
- if (second != 0) {
- sb.append(second).append("s ");
- }
-
- return sb.toString().trim();
- }
-
private static class ReportingGcMonitor extends GarbageCollectionMonitor implements GarbageCollectionMonitor.Listener {
private final SparkPlatform platform;
private final CommandResponseHandler resp;
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 6dbf913..6a76748 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
@@ -39,6 +39,7 @@ import me.lucko.spark.common.sampler.async.AsyncSampler;
import me.lucko.spark.common.sampler.node.MergeMode;
import me.lucko.spark.common.sampler.source.ClassSourceLookup;
import me.lucko.spark.common.tick.TickHook;
+import me.lucko.spark.common.util.FormatUtil;
import me.lucko.spark.common.util.MethodDisambiguator;
import me.lucko.spark.proto.SparkSamplerProtos;
@@ -68,46 +69,40 @@ import static net.kyori.adventure.text.format.NamedTextColor.WHITE;
public class SamplerModule implements CommandModule {
private static final String SPARK_SAMPLER_MEDIA_TYPE = "application/x-spark-sampler";
- /** The sampler instance currently running, if any */
- private Sampler activeSampler = null;
-
- @Override
- public void close() {
- if (this.activeSampler != null) {
- this.activeSampler.stop();
- this.activeSampler = null;
- }
- }
-
@Override
public void registerCommands(Consumer<Command> consumer) {
consumer.accept(Command.builder()
.aliases("profiler", "sampler")
- .argumentUsage("info", null)
- .argumentUsage("stop", null)
- .argumentUsage("timeout", "timeout seconds")
- .argumentUsage("thread *", null)
- .argumentUsage("thread", "thread name")
- .argumentUsage("only-ticks-over", "tick length millis")
- .argumentUsage("interval", "interval millis")
+ .allowSubCommand(true)
+ .argumentUsage("info", "", null)
+ .argumentUsage("start", "timeout", "timeout seconds")
+ .argumentUsage("start", "thread *", null)
+ .argumentUsage("start", "thread", "thread name")
+ .argumentUsage("start", "only-ticks-over", "tick length millis")
+ .argumentUsage("start", "interval", "interval millis")
+ .argumentUsage("stop", "", null)
+ .argumentUsage("cancel", "", null)
.executor(this::profiler)
.tabCompleter((platform, sender, arguments) -> {
- if (arguments.contains("--info") || arguments.contains("--cancel")) {
- return Collections.emptyList();
+ List<String> opts = Collections.emptyList();
+
+ if (arguments.size() > 0) {
+ String subCommand = arguments.get(0);
+ if (subCommand.equals("stop") || subCommand.equals("upload")) {
+ opts = new ArrayList<>(Arrays.asList("--comment", "--save-to-file"));
+ opts.removeAll(arguments);
+ }
+ if (subCommand.equals("start")) {
+ opts = new ArrayList<>(Arrays.asList("--timeout", "--regex", "--combine-all",
+ "--not-combined", "--interval", "--only-ticks-over", "--force-java-sampler"));
+ opts.removeAll(arguments);
+ opts.add("--thread"); // allowed multiple times
+ }
}
- if (arguments.contains("--stop") || arguments.contains("--upload")) {
- return TabCompleter.completeForOpts(arguments, "--comment", "--save-to-file");
- }
-
- List<String> opts = new ArrayList<>(Arrays.asList("--info", "--stop", "--cancel",
- "--timeout", "--regex", "--combine-all", "--not-combined", "--interval",
- "--only-ticks-over", "--force-java-sampler"));
- opts.removeAll(arguments);
- opts.add("--thread"); // allowed multiple times
-
return TabCompleter.create()
- .from(0, CompletionSupplier.startsWith(opts))
+ .at(0, CompletionSupplier.startsWith(Arrays.asList("info", "start", "stop", "cancel")))
+ .from(1, CompletionSupplier.startsWith(opts))
.complete(arguments);
})
.build()
@@ -115,28 +110,48 @@ public class SamplerModule implements CommandModule {
}
private void profiler(SparkPlatform platform, CommandSender sender, CommandResponseHandler resp, Arguments arguments) {
- if (arguments.boolFlag("info")) {
+ String subCommand = arguments.subCommand() == null ? "" : arguments.subCommand();
+
+ if (subCommand.equals("info") || arguments.boolFlag("info")) {
profilerInfo(platform, resp);
return;
}
- if (arguments.boolFlag("cancel")) {
- profilerCancel(resp);
+ if (subCommand.equals("cancel") || arguments.boolFlag("cancel")) {
+ profilerCancel(platform, resp);
return;
}
- if (arguments.boolFlag("stop") || arguments.boolFlag("upload")) {
+ if (subCommand.equals("stop") || arguments.boolFlag("stop") || arguments.boolFlag("upload")) {
profilerStop(platform, sender, resp, arguments);
return;
}
- profilerStart(platform, sender, resp, arguments);
+ if (subCommand.equals("start") || arguments.boolFlag("start")) {
+ profilerStart(platform, sender, resp, arguments);
+ return;
+ }
+
+ if (arguments.raw().isEmpty()) {
+ profilerInfo(platform, resp);
+ } else {
+ profilerStart(platform, sender, resp, arguments);
+ }
}
private void profilerStart(SparkPlatform platform, CommandSender sender, CommandResponseHandler resp, Arguments arguments) {
- if (this.activeSampler != null) {
- profilerInfo(platform, resp);
- return;
+ Sampler previousSampler = platform.getSamplerContainer().getActiveSampler();
+ if (previousSampler != null) {
+ if (previousSampler.isRunningInBackground()) {
+ // there is a background profiler running - stop that first
+ resp.replyPrefixed(text("Stopping the background profiler before starting... please wait"));
+ previousSampler.stop();
+ platform.getSamplerContainer().unsetActiveSampler(previousSampler);
+ } else {
+ // there is a non-background profiler running - tell the user
+ profilerInfo(platform, resp);
+ return;
+ }
}
int timeoutSeconds = arguments.intFlag("timeout");
@@ -210,7 +225,8 @@ public class SamplerModule implements CommandModule {
if (ticksOver != -1) {
builder.ticksOver(ticksOver, tickHook);
}
- Sampler sampler = this.activeSampler = builder.start(platform);
+ Sampler sampler = builder.start(platform);
+ platform.getSamplerContainer().setActiveSampler(sampler);
resp.broadcastPrefixed(text()
.append(text("Profiler is now running!", GOLD))
@@ -222,12 +238,12 @@ public class SamplerModule implements CommandModule {
if (timeoutSeconds == -1) {
resp.broadcastPrefixed(text("It will run in the background until it is stopped by an admin."));
resp.broadcastPrefixed(text("To stop the profiler and upload the results, run:"));
- resp.broadcastPrefixed(cmdPrompt("/" + platform.getPlugin().getCommandName() + " profiler --stop"));
+ resp.broadcastPrefixed(cmdPrompt("/" + platform.getPlugin().getCommandName() + " profiler stop"));
} else {
- resp.broadcastPrefixed(text("The results will be automatically returned after the profiler has been running for " + timeoutSeconds + " seconds."));
+ resp.broadcastPrefixed(text("The results will be automatically returned after the profiler has been running for " + FormatUtil.formatSeconds(timeoutSeconds) + "."));
}
- CompletableFuture<Sampler> future = this.activeSampler.getFuture();
+ CompletableFuture<Sampler> future = sampler.getFuture();
// send message if profiling fails
future.whenCompleteAsync((s, throwable) -> {
@@ -238,11 +254,7 @@ public class SamplerModule implements CommandModule {
});
// set activeSampler to null when complete.
- future.whenCompleteAsync((s, throwable) -> {
- if (sampler == this.activeSampler) {
- this.activeSampler = null;
- }
- });
+ sampler.getFuture().whenCompleteAsync((s, throwable) -> platform.getSamplerContainer().unsetActiveSampler(s));
// await the result
if (timeoutSeconds != -1) {
@@ -258,44 +270,59 @@ public class SamplerModule implements CommandModule {
}
private void profilerInfo(SparkPlatform platform, CommandResponseHandler resp) {
- if (this.activeSampler == null) {
+ Sampler sampler = platform.getSamplerContainer().getActiveSampler();
+ if (sampler == null) {
resp.replyPrefixed(text("The profiler isn't running!"));
resp.replyPrefixed(text("To start a new one, run:"));
- resp.replyPrefixed(cmdPrompt("/" + platform.getPlugin().getCommandName() + " profiler"));
+ resp.replyPrefixed(cmdPrompt("/" + platform.getPlugin().getCommandName() + " profiler start"));
} else {
resp.replyPrefixed(text("Profiler is already running!", GOLD));
- long runningTime = (System.currentTimeMillis() - this.activeSampler.getStartTime()) / 1000L;
- resp.replyPrefixed(text("So far, it has profiled for " + runningTime + " seconds."));
+ long runningTime = (System.currentTimeMillis() - sampler.getStartTime()) / 1000L;
- long timeout = this.activeSampler.getAutoEndTime();
+ if (sampler.isRunningInBackground()) {
+ resp.replyPrefixed(text()
+ .append(text("It was started "))
+ .append(text("automatically", WHITE))
+ .append(text(" when spark enabled and has been running in the background for " + FormatUtil.formatSeconds(runningTime) + "."))
+ .build()
+ );
+ } else {
+ resp.replyPrefixed(text("So far, it has profiled for " + FormatUtil.formatSeconds(runningTime) + "."));
+ }
+
+ long timeout = sampler.getAutoEndTime();
if (timeout == -1) {
resp.replyPrefixed(text("To stop the profiler and upload the results, run:"));
- resp.replyPrefixed(cmdPrompt("/" + platform.getPlugin().getCommandName() + " profiler --stop"));
+ resp.replyPrefixed(cmdPrompt("/" + platform.getPlugin().getCommandName() + " profiler stop"));
} else {
long timeoutDiff = (timeout - System.currentTimeMillis()) / 1000L;
- resp.replyPrefixed(text("It is due to complete automatically and upload results in " + timeoutDiff + " seconds."));
+ resp.replyPrefixed(text("It is due to complete automatically and upload results in " + FormatUtil.formatSeconds(timeoutDiff) + "."));
}
resp.replyPrefixed(text("To cancel the profiler without uploading the results, run:"));
- resp.replyPrefixed(cmdPrompt("/" + platform.getPlugin().getCommandName() + " profiler --cancel"));
+ resp.replyPrefixed(cmdPrompt("/" + platform.getPlugin().getCommandName() + " profiler cancel"));
}
}
- private void profilerCancel(CommandResponseHandler resp) {
- if (this.activeSampler == null) {
+ private void profilerCancel(SparkPlatform platform, CommandResponseHandler resp) {
+ Sampler sampler = platform.getSamplerContainer().getActiveSampler();
+ if (sampler == null) {
resp.replyPrefixed(text("There isn't an active profiler running."));
} else {
- close();
+ platform.getSamplerContainer().stopActiveSampler();
resp.broadcastPrefixed(text("Profiler has been cancelled.", GOLD));
}
}
private void profilerStop(SparkPlatform platform, CommandSender sender, CommandResponseHandler resp, Arguments arguments) {
- if (this.activeSampler == null) {
+ Sampler sampler = platform.getSamplerContainer().getActiveSampler();
+
+ if (sampler == null) {
resp.replyPrefixed(text("There isn't an active profiler running."));
} else {
- this.activeSampler.stop();
+ platform.getSamplerContainer().unsetActiveSampler(sampler);
+ sampler.stop();
boolean saveToFile = arguments.boolFlag("save-to-file");
if (saveToFile) {
@@ -307,8 +334,18 @@ public class SamplerModule implements CommandModule {
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, comment, mergeMode, saveToFile);
- this.activeSampler = null;
+ handleUpload(platform, resp, sampler, comment, mergeMode, saveToFile);
+
+ // if the previous sampler was running in the background, create a new one
+ if (platform.getSamplerContainer().isBackgroundProfilerEnabled()) {
+ platform.startBackgroundProfiler();
+
+ resp.broadcastPrefixed(text()
+ .append(text("Restarted the background profiler. "))
+ .append(text("(If you don't want this to happen, run: /" + platform.getPlugin().getCommandName() + " profiler cancel)", DARK_GRAY))
+ .build()
+ );
+ }
}
}
diff --git a/spark-common/src/main/java/me/lucko/spark/common/sampler/AbstractSampler.java b/spark-common/src/main/java/me/lucko/spark/common/sampler/AbstractSampler.java
index c650738..feefd66 100644
--- a/spark-common/src/main/java/me/lucko/spark/common/sampler/AbstractSampler.java
+++ b/spark-common/src/main/java/me/lucko/spark/common/sampler/AbstractSampler.java
@@ -32,8 +32,6 @@ import me.lucko.spark.common.sampler.source.ClassSourceLookup;
import me.lucko.spark.common.sampler.source.SourceMetadata;
import me.lucko.spark.common.sampler.window.ProtoTimeEncoder;
import me.lucko.spark.common.sampler.window.WindowStatisticsCollector;
-import me.lucko.spark.common.tick.TickHook;
-import me.lucko.spark.proto.SparkProtos;
import me.lucko.spark.proto.SparkSamplerProtos.SamplerData;
import me.lucko.spark.proto.SparkSamplerProtos.SamplerMetadata;
@@ -64,6 +62,9 @@ public abstract class AbstractSampler implements Sampler {
/** The unix timestamp (in millis) when this sampler should automatically complete. */
protected final long autoEndTime; // -1 for nothing
+ /** If the sampler is running in the background */
+ protected boolean background;
+
/** Collects statistics for each window in the sample */
protected final WindowStatisticsCollector windowStatisticsCollector;
@@ -73,11 +74,12 @@ public abstract class AbstractSampler implements Sampler {
/** The garbage collector statistics when profiling started */
protected Map<String, GarbageCollectorStatistics> initialGcStats;
- protected AbstractSampler(SparkPlatform platform, int interval, ThreadDumper threadDumper, long autoEndTime) {
+ protected AbstractSampler(SparkPlatform platform, SamplerSettings settings) {
this.platform = platform;
- this.interval = interval;
- this.threadDumper = threadDumper;
- this.autoEndTime = autoEndTime;
+ this.interval = settings.interval();
+ this.threadDumper = settings.threadDumper();
+ this.autoEndTime = settings.autoEndTime();
+ this.background = settings.runningInBackground();
this.windowStatisticsCollector = new WindowStatisticsCollector(platform);
}
@@ -95,6 +97,11 @@ public abstract class AbstractSampler implements Sampler {
}
@Override
+ public boolean isRunningInBackground() {
+ return this.background;
+ }
+
+ @Override
public CompletableFuture<Sampler> getFuture() {
return this.future;
}
diff --git a/spark-common/src/main/java/me/lucko/spark/common/sampler/Sampler.java b/spark-common/src/main/java/me/lucko/spark/common/sampler/Sampler.java
index e06cba6..5d2026d 100644
--- a/spark-common/src/main/java/me/lucko/spark/common/sampler/Sampler.java
+++ b/spark-common/src/main/java/me/lucko/spark/common/sampler/Sampler.java
@@ -58,6 +58,13 @@ public interface Sampler {
long getAutoEndTime();
/**
+ * If this sampler is running in the background. (wasn't started by a specific user)
+ *
+ * @return true if the sampler is running in the background
+ */
+ boolean isRunningInBackground();
+
+ /**
* Gets a future to encapsulate the completion of the sampler
*
* @return a future
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 52a7387..ec635ef 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
@@ -38,7 +38,8 @@ public class SamplerBuilder {
private boolean ignoreSleeping = false;
private boolean ignoreNative = false;
private boolean useAsyncProfiler = true;
- private long timeout = -1;
+ private long autoEndTime = -1;
+ private boolean background = false;
private ThreadDumper threadDumper = ThreadDumper.ALL;
private ThreadGrouper threadGrouper = ThreadGrouper.BY_NAME;
@@ -57,7 +58,12 @@ public class SamplerBuilder {
if (timeout <= 0) {
throw new IllegalArgumentException("timeout > 0");
}
- this.timeout = System.currentTimeMillis() + unit.toMillis(timeout);
+ this.autoEndTime = System.currentTimeMillis() + unit.toMillis(timeout);
+ return this;
+ }
+
+ public SamplerBuilder background(boolean background) {
+ this.background = background;
return this;
}
@@ -93,15 +99,24 @@ public class SamplerBuilder {
}
public Sampler start(SparkPlatform platform) {
+ boolean onlyTicksOverMode = this.ticksOver != -1 && this.tickHook != null;
+ boolean canUseAsyncProfiler = this.useAsyncProfiler &&
+ !onlyTicksOverMode &&
+ !(this.ignoreSleeping || this.ignoreNative) &&
+ !(this.threadDumper instanceof ThreadDumper.Regex) &&
+ AsyncProfilerAccess.getInstance(platform).checkSupported(platform);
+
+
int intervalMicros = (int) (this.samplingInterval * 1000d);
+ SamplerSettings settings = new SamplerSettings(intervalMicros, this.threadDumper, this.threadGrouper, this.autoEndTime, this.background);
Sampler sampler;
- if (this.ticksOver != -1 && this.tickHook != null) {
- sampler = new JavaSampler(platform, intervalMicros, this.threadDumper, this.threadGrouper, this.timeout, this.ignoreSleeping, this.ignoreNative, this.tickHook, this.ticksOver);
- } else if (this.useAsyncProfiler && !(this.threadDumper instanceof ThreadDumper.Regex) && AsyncProfilerAccess.getInstance(platform).checkSupported(platform)) {
- sampler = new AsyncSampler(platform, intervalMicros, this.threadDumper, this.threadGrouper, this.timeout);
+ if (canUseAsyncProfiler) {
+ sampler = new AsyncSampler(platform, settings);
+ } else if (onlyTicksOverMode) {
+ sampler = new JavaSampler(platform, settings, this.ignoreSleeping, this.ignoreNative, this.tickHook, this.ticksOver);
} else {
- sampler = new JavaSampler(platform, intervalMicros, this.threadDumper, this.threadGrouper, this.timeout, this.ignoreSleeping, this.ignoreNative);
+ sampler = new JavaSampler(platform, settings, this.ignoreSleeping, this.ignoreNative);
}
sampler.start();
diff --git a/spark-common/src/main/java/me/lucko/spark/common/sampler/SamplerContainer.java b/spark-common/src/main/java/me/lucko/spark/common/sampler/SamplerContainer.java
new file mode 100644
index 0000000..f56dee5
--- /dev/null
+++ b/spark-common/src/main/java/me/lucko/spark/common/sampler/SamplerContainer.java
@@ -0,0 +1,85 @@
+/*
+ * 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.sampler;
+
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * Container for the active sampler.
+ */
+public class SamplerContainer implements AutoCloseable {
+
+ private final AtomicReference<Sampler> activeSampler = new AtomicReference<>();
+ private final boolean backgroundProfilerEnabled;
+
+ public SamplerContainer(boolean backgroundProfilerEnabled) {
+ this.backgroundProfilerEnabled = backgroundProfilerEnabled;
+ }
+
+ /**
+ * Gets the active sampler, or null if a sampler is not active.
+ *
+ * @return the active sampler
+ */
+ public Sampler getActiveSampler() {
+ return this.activeSampler.get();
+ }
+
+ /**
+ * Sets the active sampler, throwing an exception if another sampler is already active.
+ *
+ * @param sampler the sampler
+ */
+ public void setActiveSampler(Sampler sampler) {
+ if (!this.activeSampler.compareAndSet(null, sampler)) {
+ throw new IllegalStateException("Attempted to set active sampler when another was already active!");
+ }
+ }
+
+ /**
+ * Unsets the active sampler, if the provided sampler is active.
+ *
+ * @param sampler the sampler
+ */
+ public void unsetActiveSampler(Sampler sampler) {
+ this.activeSampler.compareAndSet(sampler, null);
+ }
+
+ /**
+ * Stops the active sampler, if there is one.
+ */
+ public void stopActiveSampler() {
+ Sampler sampler = this.activeSampler.getAndSet(null);
+ if (sampler != null) {
+ sampler.stop();
+ }
+ }
+
+ public boolean isBackgroundProfilerEnabled() {
+ return this.backgroundProfilerEnabled;
+ }
+
+ @Override
+ public void close() {
+ stopActiveSampler();
+ }
+
+}
diff --git a/spark-common/src/main/java/me/lucko/spark/common/sampler/SamplerSettings.java b/spark-common/src/main/java/me/lucko/spark/common/sampler/SamplerSettings.java
new file mode 100644
index 0000000..6e55a43
--- /dev/null
+++ b/spark-common/src/main/java/me/lucko/spark/common/sampler/SamplerSettings.java
@@ -0,0 +1,61 @@
+/*
+ * 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.sampler;
+
+/**
+ * Base settings for all samplers
+ */
+public class SamplerSettings {
+
+ private final int interval;
+ private final ThreadDumper threadDumper;
+ private final ThreadGrouper threadGrouper;
+ private final long autoEndTime;
+ private final boolean runningInBackground;
+
+ public SamplerSettings(int interval, ThreadDumper threadDumper, ThreadGrouper threadGrouper, long autoEndTime, boolean runningInBackground) {
+ this.interval = interval;
+ this.threadDumper = threadDumper;
+ this.threadGrouper = threadGrouper;
+ this.autoEndTime = autoEndTime;
+ this.runningInBackground = runningInBackground;
+ }
+
+ public int interval() {
+ return this.interval;
+ }
+
+ public ThreadDumper threadDumper() {
+ return this.threadDumper;
+ }
+
+ public ThreadGrouper threadGrouper() {
+ return this.threadGrouper;
+ }
+
+ public long autoEndTime() {
+ return this.autoEndTime;
+ }
+
+ public boolean runningInBackground() {
+ return this.runningInBackground;
+ }
+}
diff --git a/spark-common/src/main/java/me/lucko/spark/common/sampler/aggregator/AbstractDataAggregator.java b/spark-common/src/main/java/me/lucko/spark/common/sampler/aggregator/AbstractDataAggregator.java
index ad9dee4..2c003e5 100644
--- a/spark-common/src/main/java/me/lucko/spark/common/sampler/aggregator/AbstractDataAggregator.java
+++ b/spark-common/src/main/java/me/lucko/spark/common/sampler/aggregator/AbstractDataAggregator.java
@@ -27,6 +27,7 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.IntPredicate;
/**
* Abstract implementation of {@link DataAggregator}.
@@ -52,6 +53,11 @@ public abstract class AbstractDataAggregator implements DataAggregator {
}
@Override
+ public void pruneData(IntPredicate timeWindowPredicate) {
+ this.threadData.values().removeIf(node -> node.removeTimeWindowsRecursively(timeWindowPredicate));
+ }
+
+ @Override
public List<ThreadNode> exportData() {
List<ThreadNode> data = new ArrayList<>(this.threadData.values());
for (ThreadNode node : data) {
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 5590a96..ed33204 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
@@ -24,6 +24,7 @@ import me.lucko.spark.common.sampler.node.ThreadNode;
import me.lucko.spark.proto.SparkSamplerProtos.SamplerMetadata;
import java.util.List;
+import java.util.function.IntPredicate;
/**
* Aggregates sampling data.
@@ -38,6 +39,13 @@ public interface DataAggregator {
List<ThreadNode> exportData();
/**
+ * Prunes windows of data from this aggregator if the given {@code timeWindowPredicate} returns true.
+ *
+ * @param timeWindowPredicate the predicate
+ */
+ void pruneData(IntPredicate timeWindowPredicate);
+
+ /**
* Gets metadata about the data aggregator instance.
*/
SamplerMetadata.DataAggregator getMetadata();
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 2c9bb5f..d6cfd4f 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,8 +25,7 @@ import com.google.common.util.concurrent.ThreadFactoryBuilder;
import me.lucko.spark.common.SparkPlatform;
import me.lucko.spark.common.command.sender.CommandSender;
import me.lucko.spark.common.sampler.AbstractSampler;
-import me.lucko.spark.common.sampler.ThreadDumper;
-import me.lucko.spark.common.sampler.ThreadGrouper;
+import me.lucko.spark.common.sampler.SamplerSettings;
import me.lucko.spark.common.sampler.node.MergeMode;
import me.lucko.spark.common.sampler.source.ClassSourceLookup;
import me.lucko.spark.common.sampler.window.ProfilingWindowUtils;
@@ -36,6 +35,7 @@ import me.lucko.spark.proto.SparkSamplerProtos.SamplerData;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
+import java.util.function.IntPredicate;
/**
* A sampler implementation using async-profiler.
@@ -55,10 +55,10 @@ public class AsyncSampler extends AbstractSampler {
/** The executor used for scheduling and management */
private ScheduledExecutorService scheduler;
- public AsyncSampler(SparkPlatform platform, int interval, ThreadDumper threadDumper, ThreadGrouper threadGrouper, long endTime) {
- super(platform, interval, threadDumper, endTime);
+ public AsyncSampler(SparkPlatform platform, SamplerSettings settings) {
+ super(platform, settings);
this.profilerAccess = AsyncProfilerAccess.getInstance(platform);
- this.dataAggregator = new AsyncDataAggregator(threadGrouper);
+ this.dataAggregator = new AsyncDataAggregator(settings.threadGrouper());
this.scheduler = Executors.newSingleThreadScheduledExecutor(
new ThreadFactoryBuilder().setNameFormat("spark-asyncsampler-worker-thread").build()
);
@@ -76,15 +76,20 @@ public class AsyncSampler extends AbstractSampler {
this.windowStatisticsCollector.startCountingTicks(tickHook);
}
- int window = ProfilingWindowUtils.unixMillisToWindow(System.currentTimeMillis());
+ int window = ProfilingWindowUtils.windowNow();
AsyncProfilerJob job = this.profilerAccess.startNewProfilerJob();
job.init(this.platform, this.interval, this.threadDumper, window);
job.start();
this.currentJob = job;
- // rotate the sampler job every minute to put data into a new window
- this.scheduler.scheduleAtFixedRate(this::rotateProfilerJob, 1, 1, TimeUnit.MINUTES);
+ // rotate the sampler job to put data into a new window
+ this.scheduler.scheduleAtFixedRate(
+ this::rotateProfilerJob,
+ ProfilingWindowUtils.WINDOW_SIZE_SECONDS,
+ ProfilingWindowUtils.WINDOW_SIZE_SECONDS,
+ TimeUnit.SECONDS
+ );
recordInitialGcStats();
scheduleTimeout();
@@ -117,6 +122,11 @@ public class AsyncSampler extends AbstractSampler {
// aggregate the output of the previous job
previousJob.aggregate(this.dataAggregator);
+
+ // prune data older than the history size
+ IntPredicate predicate = ProfilingWindowUtils.keepHistoryBefore(window);
+ this.dataAggregator.pruneData(predicate);
+ this.windowStatisticsCollector.pruneStatistics(predicate);
}
} catch (Throwable e) {
e.printStackTrace();
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 8c96fd3..95c3508 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
@@ -25,8 +25,7 @@ import com.google.common.util.concurrent.ThreadFactoryBuilder;
import me.lucko.spark.common.SparkPlatform;
import me.lucko.spark.common.command.sender.CommandSender;
import me.lucko.spark.common.sampler.AbstractSampler;
-import me.lucko.spark.common.sampler.ThreadDumper;
-import me.lucko.spark.common.sampler.ThreadGrouper;
+import me.lucko.spark.common.sampler.SamplerSettings;
import me.lucko.spark.common.sampler.node.MergeMode;
import me.lucko.spark.common.sampler.source.ClassSourceLookup;
import me.lucko.spark.common.sampler.window.ProfilingWindowUtils;
@@ -34,8 +33,6 @@ import me.lucko.spark.common.sampler.window.WindowStatisticsCollector;
import me.lucko.spark.common.tick.TickHook;
import me.lucko.spark.proto.SparkSamplerProtos.SamplerData;
-import org.checkerframework.checker.units.qual.A;
-
import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;
@@ -44,6 +41,7 @@ import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.IntPredicate;
/**
* A sampler implementation using Java (WarmRoast).
@@ -68,14 +66,14 @@ public class JavaSampler extends AbstractSampler implements Runnable {
/** The last window that was profiled */
private final AtomicInteger lastWindow = new AtomicInteger();
- public JavaSampler(SparkPlatform platform, int interval, ThreadDumper threadDumper, ThreadGrouper threadGrouper, long endTime, boolean ignoreSleeping, boolean ignoreNative) {
- super(platform, interval, threadDumper, endTime);
- this.dataAggregator = new SimpleDataAggregator(this.workerPool, threadGrouper, interval, ignoreSleeping, ignoreNative);
+ public JavaSampler(SparkPlatform platform, SamplerSettings settings, boolean ignoreSleeping, boolean ignoreNative) {
+ super(platform, settings);
+ this.dataAggregator = new SimpleDataAggregator(this.workerPool, settings.threadGrouper(), settings.interval(), ignoreSleeping, ignoreNative);
}
- public JavaSampler(SparkPlatform platform, int interval, ThreadDumper threadDumper, ThreadGrouper threadGrouper, long endTime, boolean ignoreSleeping, boolean ignoreNative, TickHook tickHook, int tickLengthThreshold) {
- super(platform, interval, threadDumper, endTime);
- this.dataAggregator = new TickedDataAggregator(this.workerPool, threadGrouper, interval, ignoreSleeping, ignoreNative, tickHook, tickLengthThreshold);
+ public JavaSampler(SparkPlatform platform, SamplerSettings settings, boolean ignoreSleeping, boolean ignoreNative, TickHook tickHook, int tickLengthThreshold) {
+ super(platform, settings);
+ this.dataAggregator = new TickedDataAggregator(this.workerPool, settings.threadGrouper(), settings.interval(), ignoreSleeping, ignoreNative, tickHook, tickLengthThreshold);
}
@Override
@@ -145,10 +143,17 @@ public class JavaSampler extends AbstractSampler implements Runnable {
JavaSampler.this.dataAggregator.insertData(threadInfo, this.window);
}
- // if we have just stepped over into a new window, collect statistics for the previous window
+ // if we have just stepped over into a new window...
int previousWindow = JavaSampler.this.lastWindow.getAndSet(this.window);
if (previousWindow != 0 && previousWindow != this.window) {
+
+ // collect statistics for the previous window
JavaSampler.this.windowStatisticsCollector.measureNow(previousWindow);
+
+ // prune data older than the history size
+ IntPredicate predicate = ProfilingWindowUtils.keepHistoryBefore(this.window);
+ JavaSampler.this.dataAggregator.pruneData(predicate);
+ JavaSampler.this.windowStatisticsCollector.pruneStatistics(predicate);
}
}
}
diff --git a/spark-common/src/main/java/me/lucko/spark/common/sampler/node/AbstractNode.java b/spark-common/src/main/java/me/lucko/spark/common/sampler/node/AbstractNode.java
index e6f6cf5..2e4b055 100644
--- a/spark-common/src/main/java/me/lucko/spark/common/sampler/node/AbstractNode.java
+++ b/spark-common/src/main/java/me/lucko/spark/common/sampler/node/AbstractNode.java
@@ -20,17 +20,18 @@
package me.lucko.spark.common.sampler.node;
-import me.lucko.spark.common.sampler.async.jfr.Dictionary;
import me.lucko.spark.common.sampler.window.ProtoTimeEncoder;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
+import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.LongAdder;
-import java.util.stream.IntStream;
+import java.util.function.IntPredicate;
/**
* Encapsulates a timed node in the sampling stack.
@@ -43,9 +44,9 @@ public abstract class AbstractNode {
private final Map<StackTraceNode.Description, StackTraceNode> children = new ConcurrentHashMap<>();
/** The accumulated sample time for this node, measured in microseconds */
- // long key = the window (effectively System.currentTimeMillis() / 60_000)
+ // Integer key = the window (effectively System.currentTimeMillis() / 60_000)
// LongAdder value = accumulated time in microseconds
- private final Dictionary<LongAdder> times = new Dictionary<>();
+ private final Map<Integer, LongAdder> times = new HashMap<>();
/**
* Gets the time accumulator for a given window
@@ -67,10 +68,18 @@ public abstract class AbstractNode {
*
* @return the time windows
*/
- public IntStream getTimeWindows() {
- IntStream.Builder keys = IntStream.builder();
- this.times.forEach((key, value) -> keys.add((int) key));
- return keys.build();
+ public Set<Integer> getTimeWindows() {
+ return this.times.keySet();
+ }
+
+ /**
+ * Removes time windows from this node if they pass the given {@code predicate} test.
+ *
+ * @param predicate the predicate
+ * @return true if any time windows were removed
+ */
+ public boolean removeTimeWindows(IntPredicate predicate) {
+ return this.times.keySet().removeIf(predicate::test);
}
/**
@@ -100,7 +109,7 @@ public abstract class AbstractNode {
* @param other the other node
*/
protected void merge(AbstractNode other) {
- other.times.forEach((key, value) -> getTimeAccumulator((int) key).add(value.longValue()));
+ other.times.forEach((key, value) -> getTimeAccumulator(key).add(value.longValue()));
for (Map.Entry<StackTraceNode.Description, StackTraceNode> child : other.children.entrySet()) {
resolveChild(child.getKey()).merge(child.getValue());
}
@@ -127,7 +136,6 @@ public abstract class AbstractNode {
list.add(child);
}
- //list.sort(null);
return list;
}
diff --git a/spark-common/src/main/java/me/lucko/spark/common/sampler/node/ThreadNode.java b/spark-common/src/main/java/me/lucko/spark/common/sampler/node/ThreadNode.java
index 9faece6..37ff359 100644
--- a/spark-common/src/main/java/me/lucko/spark/common/sampler/node/ThreadNode.java
+++ b/spark-common/src/main/java/me/lucko/spark/common/sampler/node/ThreadNode.java
@@ -25,9 +25,13 @@ import me.lucko.spark.common.util.IndexedListBuilder;
import me.lucko.spark.proto.SparkSamplerProtos;
import java.util.ArrayDeque;
+import java.util.Collection;
import java.util.Deque;
+import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
+import java.util.Queue;
+import java.util.function.IntPredicate;
/**
* The root of a sampling stack for a given thread / thread group.
@@ -89,6 +93,47 @@ public final class ThreadNode extends AbstractNode {
}
}
+ /**
+ * Removes time windows that match the given {@code predicate}.
+ *
+ * @param predicate the predicate to use to test the time windows
+ * @return true if this node is now empty
+ */
+ public boolean removeTimeWindowsRecursively(IntPredicate predicate) {
+ Queue<AbstractNode> queue = new ArrayDeque<>();
+ queue.add(this);
+
+ while (!queue.isEmpty()) {
+ AbstractNode node = queue.remove();
+ Collection<StackTraceNode> children = node.getChildren();
+
+ boolean needToProcessChildren = false;
+
+ for (Iterator<StackTraceNode> it = children.iterator(); it.hasNext(); ) {
+ StackTraceNode child = it.next();
+
+ boolean windowsWereRemoved = child.removeTimeWindows(predicate);
+ boolean childIsNowEmpty = child.getTimeWindows().isEmpty();
+
+ if (childIsNowEmpty) {
+ it.remove();
+ continue;
+ }
+
+ if (windowsWereRemoved) {
+ needToProcessChildren = true;
+ }
+ }
+
+ if (needToProcessChildren) {
+ queue.addAll(children);
+ }
+ }
+
+ removeTimeWindows(predicate);
+ return getTimeWindows().isEmpty();
+ }
+
public SparkSamplerProtos.ThreadNode toProto(MergeMode mergeMode, ProtoTimeEncoder timeEncoder) {
SparkSamplerProtos.ThreadNode.Builder proto = SparkSamplerProtos.ThreadNode.newBuilder()
.setName(getThreadLabel());
diff --git a/spark-common/src/main/java/me/lucko/spark/common/sampler/window/ProfilingWindowUtils.java b/spark-common/src/main/java/me/lucko/spark/common/sampler/window/ProfilingWindowUtils.java
index 109adb3..be6f08a 100644
--- a/spark-common/src/main/java/me/lucko/spark/common/sampler/window/ProfilingWindowUtils.java
+++ b/spark-common/src/main/java/me/lucko/spark/common/sampler/window/ProfilingWindowUtils.java
@@ -20,17 +20,51 @@
package me.lucko.spark.common.sampler.window;
+import me.lucko.spark.common.sampler.aggregator.DataAggregator;
+
+import java.util.function.IntPredicate;
+
public enum ProfilingWindowUtils {
;
/**
+ * The size/duration of a profiling window in seconds.
+ * (1 window = 1 minute)
+ */
+ public static final int WINDOW_SIZE_SECONDS = 60;
+
+ /**
+ * The number of windows to record in continuous profiling before data is dropped.
+ * (60 windows * 1 minute = 1 hour of profiling data)
+ */
+ public static final int HISTORY_SIZE = Integer.getInteger("spark.continuousProfilingHistorySize", 60);
+
+ /**
* Gets the profiling window for the given time in unix-millis.
*
* @param time the time in milliseconds
* @return the window
*/
public static int unixMillisToWindow(long time) {
- // one window per minute
- return (int) (time / 60_000);
+ return (int) (time / (WINDOW_SIZE_SECONDS * 1000L));
+ }
+
+ /**
+ * Gets the window at the current time.
+ *
+ * @return the window
+ */
+ public static int windowNow() {
+ return unixMillisToWindow(System.currentTimeMillis());
+ }
+
+ /**
+ * Gets a prune predicate that can be passed to {@link DataAggregator#pruneData(IntPredicate)}.
+ *
+ * @return the prune predicate
+ */
+ public static IntPredicate keepHistoryBefore(int currentWindow) {
+ // windows that were earlier than (currentWindow minus history size) should be pruned
+ return window -> window < (currentWindow - HISTORY_SIZE);
}
}
diff --git a/spark-common/src/main/java/me/lucko/spark/common/sampler/window/ProtoTimeEncoder.java b/spark-common/src/main/java/me/lucko/spark/common/sampler/window/ProtoTimeEncoder.java
index edb2309..03da075 100644
--- a/spark-common/src/main/java/me/lucko/spark/common/sampler/window/ProtoTimeEncoder.java
+++ b/spark-common/src/main/java/me/lucko/spark/common/sampler/window/ProtoTimeEncoder.java
@@ -21,7 +21,6 @@
package me.lucko.spark.common.sampler.window;
import me.lucko.spark.common.sampler.async.jfr.Dictionary;
-import me.lucko.spark.common.sampler.node.AbstractNode;
import me.lucko.spark.common.sampler.node.ThreadNode;
import java.util.HashMap;
@@ -42,7 +41,7 @@ public class ProtoTimeEncoder {
public ProtoTimeEncoder(List<ThreadNode> sourceData) {
// get an array of all keys that show up in the source data
this.keys = sourceData.stream()
- .map(AbstractNode::getTimeWindows)
+ .map(n -> n.getTimeWindows().stream().mapToInt(i -> i))
.reduce(IntStream.empty(), IntStream::concat)
.distinct()
.sorted()
@@ -70,14 +69,14 @@ public class ProtoTimeEncoder {
* @param times a dictionary of times (unix-time millis -> duration in microseconds)
* @return the times encoded as a double array
*/
- public double[] encode(Dictionary<LongAdder> times) {
+ public double[] encode(Map<Integer, LongAdder> times) {
// construct an array of values - length needs to exactly match the
// number of keys, even if some values are zero.
double[] array = new double[this.keys.length];
times.forEach((key, value) -> {
// get the index for the given key
- Integer idx = this.keysToIndex.get((int) key);
+ Integer idx = this.keysToIndex.get(key);
if (idx == null) {
throw new RuntimeException("No index for key " + key + " in " + this.keysToIndex.keySet());
}
diff --git a/spark-common/src/main/java/me/lucko/spark/common/sampler/window/WindowStatisticsCollector.java b/spark-common/src/main/java/me/lucko/spark/common/sampler/window/WindowStatisticsCollector.java
index 47f739d..7da62fa 100644
--- a/spark-common/src/main/java/me/lucko/spark/common/sampler/window/WindowStatisticsCollector.java
+++ b/spark-common/src/main/java/me/lucko/spark/common/sampler/window/WindowStatisticsCollector.java
@@ -30,6 +30,7 @@ import me.lucko.spark.proto.SparkProtos;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.IntPredicate;
/**
* Collects statistics for each profiling window.
@@ -116,6 +117,10 @@ public class WindowStatisticsCollector {
}
}
+ public void pruneStatistics(IntPredicate predicate) {
+ this.stats.keySet().removeIf(predicate::test);
+ }
+
public Map<Integer, SparkProtos.WindowStatistics> export() {
return this.stats;
}
diff --git a/spark-common/src/main/java/me/lucko/spark/common/util/Configuration.java b/spark-common/src/main/java/me/lucko/spark/common/util/Configuration.java
index 7588645..ce63878 100644
--- a/spark-common/src/main/java/me/lucko/spark/common/util/Configuration.java
+++ b/spark-common/src/main/java/me/lucko/spark/common/util/Configuration.java
@@ -67,4 +67,14 @@ public final class Configuration {
return val.isBoolean() ? val.getAsBoolean() : def;
}
+ public int getInteger(String path, int def) {
+ JsonElement el = this.root.get(path);
+ if (el == null || !el.isJsonPrimitive()) {
+ return def;
+ }
+
+ JsonPrimitive val = el.getAsJsonPrimitive();
+ return val.isBoolean() ? val.getAsInt() : def;
+ }
+
}
diff --git a/spark-common/src/main/java/me/lucko/spark/common/util/FormatUtil.java b/spark-common/src/main/java/me/lucko/spark/common/util/FormatUtil.java
index c4a3d66..1ee3b0f 100644
--- a/spark-common/src/main/java/me/lucko/spark/common/util/FormatUtil.java
+++ b/spark-common/src/main/java/me/lucko/spark/common/util/FormatUtil.java
@@ -62,4 +62,24 @@ public enum FormatUtil {
.append(Component.text(unit))
.build();
}
+
+ public static String formatSeconds(long seconds) {
+ if (seconds <= 0) {
+ return "0s";
+ }
+
+ long second = seconds;
+ long minute = second / 60;
+ second = second % 60;
+
+ StringBuilder sb = new StringBuilder();
+ if (minute != 0) {
+ sb.append(minute).append("m ");
+ }
+ if (second != 0) {
+ sb.append(second).append("s ");
+ }
+
+ return sb.toString().trim();
+ }
}