From 76f43ab59d3839600bd9e040ff2d09199ebe778a Mon Sep 17 00:00:00 2001 From: Luck Date: Sun, 13 Nov 2022 19:15:54 +0000 Subject: Limit profile length to 1 hour --- .../java/me/lucko/spark/common/SparkPlatform.java | 8 +++ .../common/command/modules/SamplerModule.java | 50 ++++++-------- .../lucko/spark/common/sampler/SamplerBuilder.java | 23 +++++-- .../spark/common/sampler/SamplerContainer.java | 76 ++++++++++++++++++++++ .../sampler/aggregator/AbstractDataAggregator.java | 6 ++ .../common/sampler/aggregator/DataAggregator.java | 8 +++ .../spark/common/sampler/async/AsyncSampler.java | 14 +++- .../spark/common/sampler/java/JavaSampler.java | 9 ++- .../spark/common/sampler/node/AbstractNode.java | 28 +++++--- .../spark/common/sampler/node/ThreadNode.java | 44 +++++++++++++ .../sampler/window/ProfilingWindowUtils.java | 38 ++++++++++- .../common/sampler/window/ProtoTimeEncoder.java | 7 +- 12 files changed, 254 insertions(+), 57 deletions(-) create mode 100644 spark-common/src/main/java/me/lucko/spark/common/sampler/SamplerContainer.java (limited to 'spark-common/src/main/java') 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..a015e42 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,7 @@ 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.SamplerContainer; import me.lucko.spark.common.sampler.source.ClassSourceLookup; import me.lucko.spark.common.tick.TickHook; import me.lucko.spark.common.tick.TickReporter; @@ -98,6 +99,7 @@ public class SparkPlatform { private final List 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 +139,8 @@ public class SparkPlatform { this.activityLog = new ActivityLog(plugin.getPluginDirectory().resolve("activity.json")); this.activityLog.load(); + this.samplerContainer = new SamplerContainer(); + this.tickHook = plugin.createTickHook(); this.tickReporter = plugin.createTickReporter(); this.tickStatistics = this.tickHook != null || this.tickReporter != null ? new TickStatistics() : null; @@ -229,6 +233,10 @@ public class SparkPlatform { return this.activityLog; } + public SamplerContainer getSamplerContainer() { + return this.samplerContainer; + } + public TickHook getTickHook() { return this.tickHook; } 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..00bf1a9 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 @@ -68,17 +68,6 @@ 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 consumer) { consumer.accept(Command.builder() @@ -121,7 +110,7 @@ public class SamplerModule implements CommandModule { } if (arguments.boolFlag("cancel")) { - profilerCancel(resp); + profilerCancel(platform, resp); return; } @@ -134,7 +123,7 @@ public class SamplerModule implements CommandModule { } private void profilerStart(SparkPlatform platform, CommandSender sender, CommandResponseHandler resp, Arguments arguments) { - if (this.activeSampler != null) { + if (platform.getSamplerContainer().getActiveSampler() != null) { profilerInfo(platform, resp); return; } @@ -210,7 +199,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)) @@ -227,7 +217,7 @@ public class SamplerModule implements CommandModule { resp.broadcastPrefixed(text("The results will be automatically returned after the profiler has been running for " + timeoutSeconds + " seconds.")); } - CompletableFuture future = this.activeSampler.getFuture(); + CompletableFuture future = sampler.getFuture(); // send message if profiling fails future.whenCompleteAsync((s, throwable) -> { @@ -238,11 +228,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,17 +244,18 @@ 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")); } else { resp.replyPrefixed(text("Profiler is already running!", GOLD)); - long runningTime = (System.currentTimeMillis() - this.activeSampler.getStartTime()) / 1000L; + long runningTime = (System.currentTimeMillis() - sampler.getStartTime()) / 1000L; resp.replyPrefixed(text("So far, it has profiled for " + runningTime + " seconds.")); - long timeout = this.activeSampler.getAutoEndTime(); + 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")); @@ -282,20 +269,24 @@ public class SamplerModule implements CommandModule { } } - 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 +298,7 @@ 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); } } 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..382950a 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 @@ -93,15 +93,28 @@ public class SamplerBuilder { } public Sampler start(SparkPlatform platform) { + boolean onlyTicksOverMode = this.ticksOver != -1 && this.tickHook != null; + boolean canUseAsyncProfiler = this.useAsyncProfiler && + !(this.ignoreSleeping || this.ignoreNative) && + !(this.threadDumper instanceof ThreadDumper.Regex) && + AsyncProfilerAccess.getInstance(platform).checkSupported(platform); + + int intervalMicros = (int) (this.samplingInterval * 1000d); 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 (onlyTicksOverMode) { + sampler = new JavaSampler(platform, intervalMicros, this.threadDumper, + this.threadGrouper, this.timeout, this.ignoreSleeping, this.ignoreNative, + this.tickHook, this.ticksOver); + + } else if (canUseAsyncProfiler) { + sampler = new AsyncSampler(platform, intervalMicros, this.threadDumper, + this.threadGrouper, this.timeout); + } else { - sampler = new JavaSampler(platform, intervalMicros, this.threadDumper, this.threadGrouper, this.timeout, this.ignoreSleeping, this.ignoreNative); + sampler = new JavaSampler(platform, intervalMicros, this.threadDumper, + this.threadGrouper, this.timeout, 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..55913d8 --- /dev/null +++ b/spark-common/src/main/java/me/lucko/spark/common/sampler/SamplerContainer.java @@ -0,0 +1,76 @@ +/* + * This file is part of spark. + * + * Copyright (c) lucko (Luck) + * 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 . + */ + +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 activeSampler = new AtomicReference<>(); + + /** + * 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(); + } + } + + @Override + public void close() { + stopActiveSampler(); + } + +} 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}. @@ -51,6 +52,11 @@ public abstract class AbstractDataAggregator implements DataAggregator { return this.threadData.computeIfAbsent(group, ThreadNode::new); } + @Override + public void pruneData(IntPredicate timeWindowPredicate) { + this.threadData.values().removeIf(node -> node.removeTimeWindowsRecursively(timeWindowPredicate)); + } + @Override public List exportData() { List data = new ArrayList<>(this.threadData.values()); 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. @@ -37,6 +38,13 @@ public interface DataAggregator { */ List 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. */ 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..cbc81c7 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 @@ -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,9 @@ public class AsyncSampler extends AbstractSampler { // aggregate the output of the previous job previousJob.aggregate(this.dataAggregator); + + // prune data older than the history size + this.dataAggregator.pruneData(ProfilingWindowUtils.keepHistoryBefore(window)); } } 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..6aad5e3 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 @@ -34,8 +34,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; @@ -145,10 +143,15 @@ 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 + JavaSampler.this.dataAggregator.pruneData(ProfilingWindowUtils.keepHistoryBefore(this.window)); } } } 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 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 times = new Dictionary<>(); + private final Map 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 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 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..5035046 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,46 @@ 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 queue = new ArrayDeque<>(); + queue.add(this); + + while (!queue.isEmpty()) { + AbstractNode node = queue.remove(); + Collection children = node.getChildren(); + + boolean needToProcessChildren = false; + + for (Iterator 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); + } + } + + 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,9 +20,25 @@ 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. * @@ -30,7 +46,25 @@ public enum ProfilingWindowUtils { * @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 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 times) { + public double[] encode(Map 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()); } -- cgit From f2d77d875f32f107987c93da1f90529fc6812444 Mon Sep 17 00:00:00 2001 From: Luck Date: Sun, 13 Nov 2022 21:24:57 +0000 Subject: Background profiler --- .../java/me/lucko/spark/common/SparkPlatform.java | 82 ++++++++++----- .../me/lucko/spark/common/command/Arguments.java | 11 +- .../me/lucko/spark/common/command/Command.java | 58 ++++++++++- .../common/command/modules/GcMonitoringModule.java | 22 +--- .../common/command/modules/SamplerModule.java | 115 +++++++++++++++------ .../spark/common/sampler/AbstractSampler.java | 19 ++-- .../me/lucko/spark/common/sampler/Sampler.java | 7 ++ .../lucko/spark/common/sampler/SamplerBuilder.java | 28 ++--- .../spark/common/sampler/SamplerContainer.java | 9 ++ .../spark/common/sampler/SamplerSettings.java | 61 +++++++++++ .../spark/common/sampler/async/AsyncSampler.java | 14 +-- .../spark/common/sampler/java/JavaSampler.java | 20 ++-- .../spark/common/sampler/node/ThreadNode.java | 1 + .../sampler/window/WindowStatisticsCollector.java | 5 + .../me/lucko/spark/common/util/Configuration.java | 10 ++ .../me/lucko/spark/common/util/FormatUtil.java | 20 ++++ 16 files changed, 362 insertions(+), 120 deletions(-) create mode 100644 spark-common/src/main/java/me/lucko/spark/common/sampler/SamplerSettings.java (limited to 'spark-common/src/main/java') 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 a015e42..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,7 +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; @@ -64,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; @@ -74,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; @@ -139,7 +142,7 @@ public class SparkPlatform { this.activityLog = new ActivityLog(plugin.getPluginDirectory().resolve("activity.json")); this.activityLog.load(); - this.samplerContainer = new SamplerContainer(); + this.samplerContainer = new SamplerContainer(this.configuration.getBoolean("backgroundProfiler", true)); this.tickHook = plugin.createTickHook(); this.tickReporter = plugin.createTickReporter(); @@ -179,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() { @@ -196,6 +209,8 @@ public class SparkPlatform { module.close(); } + this.samplerContainer.close(); + SparkApi.unregister(); this.temporaryFiles.deleteTemporaryFiles(); @@ -269,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 { @@ -394,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)); } @@ -442,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> 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 rawArgs; private final SetMultimap parsedArgs; + private String parsedSubCommand = null; - public Arguments(List rawArgs) { + public Arguments(List 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 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 arguments; private final Executor executor; private final TabCompleter tabCompleter; + private final boolean allowSubCommand; - private Command(List aliases, List arguments, Executor executor, TabCompleter tabCompleter) { + private Command(List aliases, List arguments, Executor executor, TabCompleter tabCompleter, boolean allowSubCommand) { this.aliases = aliases; this.arguments = arguments; this.executor = executor; this.tabCompleter = tabCompleter; + this.allowSubCommand = allowSubCommand; } public List 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 aliases = ImmutableList.builder(); private final ImmutableList.Builder 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 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 00bf1a9..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; @@ -72,31 +73,36 @@ public class SamplerModule implements CommandModule { public void registerCommands(Consumer 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 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 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() @@ -104,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")) { + 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 (platform.getSamplerContainer().getActiveSampler() != 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"); @@ -212,9 +238,9 @@ 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 future = sampler.getFuture(); @@ -248,24 +274,34 @@ public class SamplerModule implements CommandModule { 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() - sampler.getStartTime()) / 1000L; - resp.replyPrefixed(text("So far, it has profiled for " + runningTime + " seconds.")); + + 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")); } } @@ -299,6 +335,17 @@ public class SamplerModule implements CommandModule { MethodDisambiguator methodDisambiguator = new MethodDisambiguator(); MergeMode mergeMode = arguments.boolFlag("separate-parent-calls") ? MergeMode.separateParentCalls(methodDisambiguator) : MergeMode.sameMethod(methodDisambiguator); 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 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); } @@ -94,6 +96,11 @@ public abstract class AbstractSampler implements Sampler { return this.autoEndTime; } + @Override + public boolean isRunningInBackground() { + return this.background; + } + @Override public CompletableFuture 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 @@ -57,6 +57,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 * 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 382950a..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; } @@ -95,26 +101,22 @@ 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 (onlyTicksOverMode) { - sampler = new JavaSampler(platform, intervalMicros, this.threadDumper, - this.threadGrouper, this.timeout, this.ignoreSleeping, this.ignoreNative, - this.tickHook, this.ticksOver); - - } else if (canUseAsyncProfiler) { - 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 index 55913d8..f56dee5 100644 --- 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 @@ -28,6 +28,11 @@ import java.util.concurrent.atomic.AtomicReference; public class SamplerContainer implements AutoCloseable { private final AtomicReference 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. @@ -68,6 +73,10 @@ public class SamplerContainer implements AutoCloseable { } } + 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) + * 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 . + */ + +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/async/AsyncSampler.java b/spark-common/src/main/java/me/lucko/spark/common/sampler/async/AsyncSampler.java index cbc81c7..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() ); @@ -124,7 +124,9 @@ public class AsyncSampler extends AbstractSampler { previousJob.aggregate(this.dataAggregator); // prune data older than the history size - this.dataAggregator.pruneData(ProfilingWindowUtils.keepHistoryBefore(window)); + 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 6aad5e3..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; @@ -42,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). @@ -66,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 @@ -151,7 +151,9 @@ public class JavaSampler extends AbstractSampler implements Runnable { JavaSampler.this.windowStatisticsCollector.measureNow(previousWindow); // prune data older than the history size - JavaSampler.this.dataAggregator.pruneData(ProfilingWindowUtils.keepHistoryBefore(this.window)); + 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/ThreadNode.java b/spark-common/src/main/java/me/lucko/spark/common/sampler/node/ThreadNode.java index 5035046..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 @@ -130,6 +130,7 @@ public final class ThreadNode extends AbstractNode { } } + removeTimeWindows(predicate); return getTimeWindows().isEmpty(); } 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 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(); + } } -- cgit