From 189aa69a0d0b56c13109d96c3ef46fce53fb835f Mon Sep 17 00:00:00 2001 From: Bram Ceulemans Date: Thu, 19 Aug 2021 18:34:29 +0200 Subject: [PATCH 1/4] Work in progress Profiler API --- .../main/java/me/lucko/spark/api/Spark.java | 8 +- .../spark/api/profiler/DumperChoice.java | 4 + .../spark/api/profiler/GrouperChoice.java | 10 ++ .../me/lucko/spark/api/profiler/Profiler.java | 8 + .../api/profiler/ProfilerConfiguration.java | 73 +++++++++ .../ProfilerConfigurationBuilder.java | 101 ++++++++++++ .../spark/api/profiler/ProfilerReport.java | 4 + .../me/lucko/spark/common/api/SparkApi.java | 8 +- .../common/command/modules/SamplerModule.java | 54 +++---- .../spark/common/sampler/SamplerBuilder.java | 3 +- .../spark/common/sampler/ThreadDumper.java | 146 ++---------------- .../spark/common/sampler/ThreadGrouper.java | 87 +++-------- .../common/sampler/async/AsyncSampler.java | 11 +- .../sampler/dumper/PatternThreadDumper.java | 69 +++++++++ .../sampler/dumper/SpecificThreadDumper.java | 64 ++++++++ .../sampler/grouper/NameThreadGrouper.java | 19 +++ .../sampler/grouper/PoolThreadGrouper.java | 44 ++++++ .../sampler/grouper/SingleThreadGrouper.java | 21 +++ 18 files changed, 495 insertions(+), 239 deletions(-) create mode 100644 spark-api/src/main/java/me/lucko/spark/api/profiler/DumperChoice.java create mode 100644 spark-api/src/main/java/me/lucko/spark/api/profiler/GrouperChoice.java create mode 100644 spark-api/src/main/java/me/lucko/spark/api/profiler/Profiler.java create mode 100644 spark-api/src/main/java/me/lucko/spark/api/profiler/ProfilerConfiguration.java create mode 100644 spark-api/src/main/java/me/lucko/spark/api/profiler/ProfilerConfigurationBuilder.java create mode 100644 spark-api/src/main/java/me/lucko/spark/api/profiler/ProfilerReport.java create mode 100644 spark-common/src/main/java/me/lucko/spark/common/sampler/dumper/PatternThreadDumper.java create mode 100644 spark-common/src/main/java/me/lucko/spark/common/sampler/dumper/SpecificThreadDumper.java create mode 100644 spark-common/src/main/java/me/lucko/spark/common/sampler/grouper/NameThreadGrouper.java create mode 100644 spark-common/src/main/java/me/lucko/spark/common/sampler/grouper/PoolThreadGrouper.java create mode 100644 spark-common/src/main/java/me/lucko/spark/common/sampler/grouper/SingleThreadGrouper.java diff --git a/spark-api/src/main/java/me/lucko/spark/api/Spark.java b/spark-api/src/main/java/me/lucko/spark/api/Spark.java index 8620348d..333d8cfc 100644 --- a/spark-api/src/main/java/me/lucko/spark/api/Spark.java +++ b/spark-api/src/main/java/me/lucko/spark/api/Spark.java @@ -26,11 +26,11 @@ package me.lucko.spark.api; import me.lucko.spark.api.gc.GarbageCollector; +import me.lucko.spark.api.profiler.Profiler; import me.lucko.spark.api.statistic.StatisticWindow; import me.lucko.spark.api.statistic.misc.DoubleAverageInfo; import me.lucko.spark.api.statistic.types.DoubleStatistic; import me.lucko.spark.api.statistic.types.GenericStatistic; - import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.jetbrains.annotations.Unmodifiable; @@ -81,4 +81,10 @@ public interface Spark { */ @NonNull @Unmodifiable Map gc(); + /** + * Get the {@link Profiler} API. + * + * @return the profiler api + */ + @NonNull Profiler profiler(); } diff --git a/spark-api/src/main/java/me/lucko/spark/api/profiler/DumperChoice.java b/spark-api/src/main/java/me/lucko/spark/api/profiler/DumperChoice.java new file mode 100644 index 00000000..93b18029 --- /dev/null +++ b/spark-api/src/main/java/me/lucko/spark/api/profiler/DumperChoice.java @@ -0,0 +1,4 @@ +package me.lucko.spark.api.profiler; + +public final class DumperChoice { +} diff --git a/spark-api/src/main/java/me/lucko/spark/api/profiler/GrouperChoice.java b/spark-api/src/main/java/me/lucko/spark/api/profiler/GrouperChoice.java new file mode 100644 index 00000000..d0a46b05 --- /dev/null +++ b/spark-api/src/main/java/me/lucko/spark/api/profiler/GrouperChoice.java @@ -0,0 +1,10 @@ +package me.lucko.spark.api.profiler; + +/** + * Group the threads by a specific mode. Either grouping them by name, pool or combined into a single group. + */ +public enum GrouperChoice { + SINGLE, + NAME, + POOL +} diff --git a/spark-api/src/main/java/me/lucko/spark/api/profiler/Profiler.java b/spark-api/src/main/java/me/lucko/spark/api/profiler/Profiler.java new file mode 100644 index 00000000..995f1027 --- /dev/null +++ b/spark-api/src/main/java/me/lucko/spark/api/profiler/Profiler.java @@ -0,0 +1,8 @@ +package me.lucko.spark.api.profiler; + +public interface Profiler { + + boolean start(ProfilerConfiguration configuration); + + ProfilerReport stop(); +} diff --git a/spark-api/src/main/java/me/lucko/spark/api/profiler/ProfilerConfiguration.java b/spark-api/src/main/java/me/lucko/spark/api/profiler/ProfilerConfiguration.java new file mode 100644 index 00000000..7fe4b582 --- /dev/null +++ b/spark-api/src/main/java/me/lucko/spark/api/profiler/ProfilerConfiguration.java @@ -0,0 +1,73 @@ +package me.lucko.spark.api.profiler; + +import org.jetbrains.annotations.Nullable; + +import java.time.Duration; + +public interface ProfilerConfiguration { + + static ProfilerConfigurationBuilder builder() { + return new ProfilerConfigurationBuilder(); + } + + /** + * Get the interval (in millis) of when the profiler should take samples. + * + * @return the sample interval + */ + double interval(); + + /** + * Get if sleeping threads should be ignored. + * + * @return if sleeping threads are ignored + */ + boolean ignoreSleeping(); + + /** + * Get if native threads should be ignored. + * + * @return if native threads are ignored + */ + boolean ignoreNative(); + + /** + * Get if the native Java sampler should be used. + * + * @return if the native Java sampler is used + */ + boolean forceJavaSampler(); + + /** + * Minimum duration (in millis) a tick has to take in order to be recorded. + * + * @return the minimum tick duration + */ + int minimumTickDuration(); + + /** + * Get how long the profiler should run, if the duration is null, the profiler runs indefinite. + * + * @return duration of the profile or null if indefinite + */ + @Nullable + Duration duration(); + + /** + * Get the choice of which dumper to use (i.e. ALL, Regex or Specific). + * If no dumper is defined, ALL is used. + * + * @return the thread dumper choice + */ + @Nullable + DumperChoice dumper(); + + /** + * Get the choice of which thread grouper (AS_ONE, BY_NAME, BY_POOL) to use for this profiler. + * If the grouper is null, BY_POOL is used. + * + * @return the thread grouper choice + */ + @Nullable + GrouperChoice grouper(); +} diff --git a/spark-api/src/main/java/me/lucko/spark/api/profiler/ProfilerConfigurationBuilder.java b/spark-api/src/main/java/me/lucko/spark/api/profiler/ProfilerConfigurationBuilder.java new file mode 100644 index 00000000..0b3dec52 --- /dev/null +++ b/spark-api/src/main/java/me/lucko/spark/api/profiler/ProfilerConfigurationBuilder.java @@ -0,0 +1,101 @@ +package me.lucko.spark.api.profiler; + +import org.jetbrains.annotations.Nullable; + +import java.time.Duration; + +public class ProfilerConfigurationBuilder { + + private double interval = 5; + private boolean ignoreSleeping = false; + private boolean ignoreNative = false; + private boolean forceJavaSampler = true; + private int minimumTickDuration = -1; + private @Nullable Duration duration = null; + private @Nullable DumperChoice dumper = null; + private @Nullable GrouperChoice grouper = null; + + public ProfilerConfigurationBuilder interval(double interval) { + this.interval = interval; + return this; + } + + public ProfilerConfigurationBuilder ignoreSleeping(boolean ignoreSleeping) { + this.ignoreSleeping = ignoreSleeping; + return this; + } + + public ProfilerConfigurationBuilder ignoreNative(boolean ignoreNative) { + this.ignoreNative = ignoreNative; + return this; + } + + public ProfilerConfigurationBuilder forceJavaSampler(boolean forceJavaSampler) { + this.forceJavaSampler = forceJavaSampler; + return this; + } + + public ProfilerConfigurationBuilder minimumTickDuration(int minimumTickDuration) { + this.minimumTickDuration = minimumTickDuration; + return this; + } + + public ProfilerConfigurationBuilder duration(Duration duration) { + this.duration = duration; + return this; + } + + public ProfilerConfigurationBuilder dumper(DumperChoice dumper) { + this.dumper = dumper; + return this; + } + + public ProfilerConfigurationBuilder grouper(GrouperChoice grouper) { + this.grouper = grouper; + return this; + } + + public ProfilerConfiguration build() { + return new ProfilerConfiguration() { + @Override + public double interval() { + return interval; + } + + @Override + public boolean ignoreSleeping() { + return ignoreSleeping; + } + + @Override + public boolean ignoreNative() { + return ignoreNative; + } + + @Override + public boolean forceJavaSampler() { + return forceJavaSampler; + } + + @Override + public int minimumTickDuration() { + return minimumTickDuration; + } + + @Override + public @Nullable Duration duration() { + return duration; + } + + @Override + public @Nullable DumperChoice dumper() { + return dumper; + } + + @Override + public @Nullable GrouperChoice grouper() { + return grouper; + } + }; + } +} diff --git a/spark-api/src/main/java/me/lucko/spark/api/profiler/ProfilerReport.java b/spark-api/src/main/java/me/lucko/spark/api/profiler/ProfilerReport.java new file mode 100644 index 00000000..154513c6 --- /dev/null +++ b/spark-api/src/main/java/me/lucko/spark/api/profiler/ProfilerReport.java @@ -0,0 +1,4 @@ +package me.lucko.spark.api.profiler; + +public interface ProfilerReport { +} diff --git a/spark-common/src/main/java/me/lucko/spark/common/api/SparkApi.java b/spark-common/src/main/java/me/lucko/spark/common/api/SparkApi.java index 5ac41fc3..d65e9daa 100644 --- a/spark-common/src/main/java/me/lucko/spark/common/api/SparkApi.java +++ b/spark-common/src/main/java/me/lucko/spark/common/api/SparkApi.java @@ -25,6 +25,7 @@ import me.lucko.spark.api.Spark; import me.lucko.spark.api.SparkProvider; import me.lucko.spark.api.gc.GarbageCollector; +import me.lucko.spark.api.profiler.Profiler; import me.lucko.spark.api.statistic.misc.DoubleAverageInfo; import me.lucko.spark.api.statistic.types.DoubleStatistic; import me.lucko.spark.api.statistic.types.GenericStatistic; @@ -37,9 +38,7 @@ import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; -import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; -import java.sql.Ref; import java.util.HashMap; import java.util.Map; @@ -171,6 +170,11 @@ public DoubleAverageInfo poll(StatisticWindow.@NonNull MillisPerTick window) { return ImmutableMap.copyOf(map); } + @Override + public @NonNull Profiler profiler() { + return null; + } + public static void register(Spark spark) { try { SINGLETON_SET_METHOD.invoke(null, spark); 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 c0a295ab..97219dc0 100644 --- a/spark-common/src/main/java/me/lucko/spark/common/command/modules/SamplerModule.java +++ b/spark-common/src/main/java/me/lucko/spark/common/command/modules/SamplerModule.java @@ -21,7 +21,7 @@ package me.lucko.spark.common.command.modules; import com.google.common.collect.Iterables; - +import me.lucko.spark.api.profiler.GrouperChoice; import me.lucko.spark.common.SparkPlatform; import me.lucko.spark.common.activitylog.Activity; import me.lucko.spark.common.command.Arguments; @@ -31,44 +31,35 @@ import me.lucko.spark.common.command.sender.CommandSender; import me.lucko.spark.common.command.tabcomplete.CompletionSupplier; import me.lucko.spark.common.command.tabcomplete.TabCompleter; -import me.lucko.spark.common.sampler.Sampler; -import me.lucko.spark.common.sampler.SamplerBuilder; -import me.lucko.spark.common.sampler.ThreadDumper; -import me.lucko.spark.common.sampler.ThreadGrouper; -import me.lucko.spark.common.sampler.ThreadNodeOrder; +import me.lucko.spark.common.sampler.*; import me.lucko.spark.common.sampler.async.AsyncSampler; +import me.lucko.spark.common.sampler.dumper.PatternThreadDumper; +import me.lucko.spark.common.sampler.dumper.SpecificThreadDumper; import me.lucko.spark.common.sampler.node.MergeMode; import me.lucko.spark.common.tick.TickHook; import me.lucko.spark.common.util.MethodDisambiguator; import me.lucko.spark.proto.SparkProtos; - import net.kyori.adventure.text.event.ClickEvent; - import okhttp3.MediaType; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Set; +import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; import static net.kyori.adventure.text.Component.space; import static net.kyori.adventure.text.Component.text; -import static net.kyori.adventure.text.format.NamedTextColor.DARK_GRAY; -import static net.kyori.adventure.text.format.NamedTextColor.GOLD; -import static net.kyori.adventure.text.format.NamedTextColor.GRAY; -import static net.kyori.adventure.text.format.NamedTextColor.RED; +import static net.kyori.adventure.text.format.NamedTextColor.*; public class SamplerModule implements CommandModule { private static final MediaType SPARK_SAMPLER_MEDIA_TYPE = MediaType.parse("application/x-spark-sampler"); - /** The sampler instance currently running, if any */ + /** + * The sampler instance currently running, if any + */ private Sampler activeSampler = null; @Override @@ -171,20 +162,20 @@ private void profilerStart(SparkPlatform platform, CommandSender sender, Command threadDumper = ThreadDumper.ALL; } else { if (arguments.boolFlag("regex")) { - threadDumper = new ThreadDumper.Regex(threads); + threadDumper = new PatternThreadDumper(threads); } else { // specific matches - threadDumper = new ThreadDumper.Specific(threads); + threadDumper = new SpecificThreadDumper(threads); } } ThreadGrouper threadGrouper; if (arguments.boolFlag("combine-all")) { - threadGrouper = ThreadGrouper.AS_ONE; + threadGrouper = ThreadGrouper.get(GrouperChoice.SINGLE); } else if (arguments.boolFlag("not-combined")) { - threadGrouper = ThreadGrouper.BY_NAME; + threadGrouper = ThreadGrouper.get(GrouperChoice.NAME); } else { - threadGrouper = ThreadGrouper.BY_POOL; + threadGrouper = ThreadGrouper.get(GrouperChoice.POOL); } int ticksOver = arguments.intFlag("only-ticks-over"); @@ -204,16 +195,17 @@ private void profilerStart(SparkPlatform platform, CommandSender sender, Command resp.broadcastPrefixed(text("Initializing a new profiler, please wait...")); - SamplerBuilder builder = new SamplerBuilder(); - builder.threadDumper(threadDumper); - builder.threadGrouper(threadGrouper); + SamplerBuilder builder = new SamplerBuilder() + .threadDumper(threadDumper) + .threadGrouper(threadGrouper) + .samplingInterval(intervalMillis) + .ignoreSleeping(ignoreSleeping) + .ignoreNative(ignoreNative) + .forceJavaSampler(forceJavaSampler); + if (timeoutSeconds != -1) { builder.completeAfter(timeoutSeconds, TimeUnit.SECONDS); } - builder.samplingInterval(intervalMillis); - builder.ignoreSleeping(ignoreSleeping); - builder.ignoreNative(ignoreNative); - builder.forceJavaSampler(forceJavaSampler); if (ticksOver != -1) { builder.ticksOver(ticksOver, tickHook); } @@ -236,7 +228,7 @@ private void profilerStart(SparkPlatform platform, CommandSender sender, Command // send message if profiling fails future.whenCompleteAsync((s, throwable) -> { if (throwable != null) { - resp.broadcastPrefixed(text("Profiler operation failed unexpectedly. Error: " + throwable.toString(), RED)); + resp.broadcastPrefixed(text("Profiler operation failed unexpectedly. Error: " + throwable, RED)); throwable.printStackTrace(); } }); 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 7dff29e3..01cf5e16 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 @@ -22,6 +22,7 @@ import me.lucko.spark.common.sampler.async.AsyncProfilerAccess; import me.lucko.spark.common.sampler.async.AsyncSampler; +import me.lucko.spark.common.sampler.dumper.PatternThreadDumper; import me.lucko.spark.common.sampler.java.JavaSampler; import me.lucko.spark.common.tick.TickHook; @@ -95,7 +96,7 @@ public Sampler start() { int intervalMicros = (int) (this.samplingInterval * 1000d); if (this.ticksOver == -1 || this.tickHook == null) { - if (this.useAsyncProfiler && this.timeout == -1 && !(this.threadDumper instanceof ThreadDumper.Regex) && AsyncProfilerAccess.INSTANCE.isSupported()) { + if (this.useAsyncProfiler && this.timeout == -1 && !(this.threadDumper instanceof PatternThreadDumper) && AsyncProfilerAccess.INSTANCE.isSupported()) { sampler = new AsyncSampler(intervalMicros, this.threadDumper, this.threadGrouper); } else { sampler = new JavaSampler(intervalMicros, this.threadDumper, this.threadGrouper, this.timeout, this.ignoreSleeping, this.ignoreNative); diff --git a/spark-common/src/main/java/me/lucko/spark/common/sampler/ThreadDumper.java b/spark-common/src/main/java/me/lucko/spark/common/sampler/ThreadDumper.java index e99114a9..e12320aa 100644 --- a/spark-common/src/main/java/me/lucko/spark/common/sampler/ThreadDumper.java +++ b/spark-common/src/main/java/me/lucko/spark/common/sampler/ThreadDumper.java @@ -21,20 +21,13 @@ package me.lucko.spark.common.sampler; -import me.lucko.spark.common.util.ThreadFinder; +import me.lucko.spark.common.sampler.dumper.SpecificThreadDumper; import me.lucko.spark.proto.SparkProtos.SamplerMetadata; import java.lang.management.ThreadInfo; import java.lang.management.ThreadMXBean; -import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; import java.util.Objects; -import java.util.Set; import java.util.function.Supplier; -import java.util.regex.Pattern; -import java.util.regex.PatternSyntaxException; -import java.util.stream.Collectors; /** * Uses the {@link ThreadMXBean} to generate {@link ThreadInfo} instances for the threads being @@ -42,19 +35,6 @@ */ public interface ThreadDumper { - /** - * Generates {@link ThreadInfo} data for the sampled threads. - * - * @param threadBean the thread bean instance to obtain the data from - * @return an array of generated thread info instances - */ - ThreadInfo[] dumpThreads(ThreadMXBean threadBean); - - /** - * Gets metadata about the thread dumper instance. - */ - SamplerMetadata.ThreadDumper getMetadata(); - /** * Implementation of {@link ThreadDumper} that generates data for all threads. */ @@ -72,12 +52,25 @@ public SamplerMetadata.ThreadDumper getMetadata() { } }; + /** + * Generates {@link ThreadInfo} data for the sampled threads. + * + * @param threadBean the thread bean instance to obtain the data from + * @return an array of generated thread info instances + */ + ThreadInfo[] dumpThreads(ThreadMXBean threadBean); + + /** + * Gets metadata about the thread dumper instance. + */ + SamplerMetadata.ThreadDumper getMetadata(); + /** * Utility to cache the creation of a {@link ThreadDumper} targeting * the game (server/client) thread. */ final class GameThread implements Supplier { - private Specific dumper = null; + private ThreadDumper dumper = null; @Override public ThreadDumper get() { @@ -86,115 +79,8 @@ public ThreadDumper get() { public void ensureSetup() { if (this.dumper == null) { - this.dumper = new Specific(new long[]{Thread.currentThread().getId()}); - } - } - } - - /** - * Implementation of {@link ThreadDumper} that generates data for a specific set of threads. - */ - final class Specific implements ThreadDumper { - private final long[] ids; - private Set threads; - private Set threadNamesLowerCase; - - public Specific(long[] ids) { - this.ids = ids; - } - - public Specific(Set names) { - this.threadNamesLowerCase = names.stream().map(String::toLowerCase).collect(Collectors.toSet()); - this.ids = new ThreadFinder().getThreads() - .filter(t -> this.threadNamesLowerCase.contains(t.getName().toLowerCase())) - .mapToLong(Thread::getId) - .toArray(); - Arrays.sort(this.ids); - } - - public Set getThreads() { - if (this.threads == null) { - this.threads = new ThreadFinder().getThreads() - .filter(t -> Arrays.binarySearch(this.ids, t.getId()) >= 0) - .collect(Collectors.toSet()); + this.dumper = new SpecificThreadDumper(new long[]{Thread.currentThread().getId()}); } - return this.threads; - } - - public Set getThreadNames() { - if (this.threadNamesLowerCase == null) { - this.threadNamesLowerCase = getThreads().stream() - .map(t -> t.getName().toLowerCase()) - .collect(Collectors.toSet()); - } - return this.threadNamesLowerCase; - } - - @Override - public ThreadInfo[] dumpThreads(ThreadMXBean threadBean) { - return threadBean.getThreadInfo(this.ids, Integer.MAX_VALUE); - } - - @Override - public SamplerMetadata.ThreadDumper getMetadata() { - return SamplerMetadata.ThreadDumper.newBuilder() - .setType(SamplerMetadata.ThreadDumper.Type.SPECIFIC) - .addAllIds(Arrays.stream(this.ids).boxed().collect(Collectors.toList())) - .build(); } } - - /** - * Implementation of {@link ThreadDumper} that generates data for a regex matched set of threads. - */ - final class Regex implements ThreadDumper { - private final ThreadFinder threadFinder = new ThreadFinder(); - private final Set namePatterns; - private final Map cache = new HashMap<>(); - - public Regex(Set namePatterns) { - this.namePatterns = namePatterns.stream() - .map(regex -> { - try { - return Pattern.compile(regex, Pattern.CASE_INSENSITIVE); - } catch (PatternSyntaxException e) { - return null; - } - }) - .filter(Objects::nonNull) - .collect(Collectors.toSet()); - } - - @Override - public ThreadInfo[] dumpThreads(ThreadMXBean threadBean) { - return this.threadFinder.getThreads() - .filter(thread -> { - Boolean result = this.cache.get(thread.getId()); - if (result != null) { - return result; - } - - for (Pattern pattern : this.namePatterns) { - if (pattern.matcher(thread.getName()).matches()) { - this.cache.put(thread.getId(), true); - return true; - } - } - this.cache.put(thread.getId(), false); - return false; - }) - .map(thread -> threadBean.getThreadInfo(thread.getId(), Integer.MAX_VALUE)) - .filter(Objects::nonNull) - .toArray(ThreadInfo[]::new); - } - - @Override - public SamplerMetadata.ThreadDumper getMetadata() { - return SamplerMetadata.ThreadDumper.newBuilder() - .setType(SamplerMetadata.ThreadDumper.Type.REGEX) - .addAllPatterns(this.namePatterns.stream().map(Pattern::pattern).collect(Collectors.toList())) - .build(); - } - } - } diff --git a/spark-common/src/main/java/me/lucko/spark/common/sampler/ThreadGrouper.java b/spark-common/src/main/java/me/lucko/spark/common/sampler/ThreadGrouper.java index e63ebc8a..aa7f2c6f 100644 --- a/spark-common/src/main/java/me/lucko/spark/common/sampler/ThreadGrouper.java +++ b/spark-common/src/main/java/me/lucko/spark/common/sampler/ThreadGrouper.java @@ -20,87 +20,36 @@ package me.lucko.spark.common.sampler; +import me.lucko.spark.api.profiler.GrouperChoice; +import me.lucko.spark.common.sampler.grouper.NameThreadGrouper; +import me.lucko.spark.common.sampler.grouper.PoolThreadGrouper; +import me.lucko.spark.common.sampler.grouper.SingleThreadGrouper; import me.lucko.spark.proto.SparkProtos.SamplerMetadata; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - /** * Function for grouping threads together */ public interface ThreadGrouper { - /** - * Implementation of {@link ThreadGrouper} that just groups by thread name. - */ - ThreadGrouper BY_NAME = new ThreadGrouper() { - @Override - public String getGroup(long threadId, String threadName) { - return threadName; - } - - @Override - public SamplerMetadata.DataAggregator.ThreadGrouper asProto() { - return SamplerMetadata.DataAggregator.ThreadGrouper.BY_NAME; - } - }; - - /** - * Implementation of {@link ThreadGrouper} that attempts to group by the name of the pool - * the thread originated from. - * - *

The regex pattern used to match pools expects a digit at the end of the thread name, - * separated from the pool name with any of one or more of ' ', '-', or '#'.

- */ - ThreadGrouper BY_POOL = new ThreadGrouper() { - private final Map cache = new ConcurrentHashMap<>(); - private final Pattern pattern = Pattern.compile("^(.*?)[-# ]+\\d+$"); - - @Override - public String getGroup(long threadId, String threadName) { - String group = this.cache.get(threadId); - if (group != null) { - return group; - } - - Matcher matcher = this.pattern.matcher(threadName); - if (!matcher.matches()) { - return threadName; - } - - group = matcher.group(1).trim() + " (Combined)"; - this.cache.put(threadId, group); // we don't care about race conditions here - return group; - } - - @Override - public SamplerMetadata.DataAggregator.ThreadGrouper asProto() { - return SamplerMetadata.DataAggregator.ThreadGrouper.BY_POOL; - } - }; - - /** - * Implementation of {@link ThreadGrouper} which groups all threads as one, under - * the name "All". - */ - ThreadGrouper AS_ONE = new ThreadGrouper() { - @Override - public String getGroup(long threadId, String threadName) { - return "All"; - } - - @Override - public SamplerMetadata.DataAggregator.ThreadGrouper asProto() { - return SamplerMetadata.DataAggregator.ThreadGrouper.AS_ONE; + ThreadGrouper BY_NAME = new NameThreadGrouper(); + ThreadGrouper BY_POOL = new PoolThreadGrouper(); + ThreadGrouper AS_ONE = new SingleThreadGrouper(); + + static ThreadGrouper get(GrouperChoice choice) { + switch (choice) { + case SINGLE: + return AS_ONE; + case NAME: + return BY_NAME; + default: + return BY_POOL; } - }; + } /** * Gets the group for the given thread. * - * @param threadId the id of the thread + * @param threadId the id of the thread * @param threadName the name of the thread * @return the group */ 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 ca30df0c..0698cc0a 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 @@ -26,6 +26,7 @@ import me.lucko.spark.common.sampler.ThreadDumper; import me.lucko.spark.common.sampler.ThreadGrouper; import me.lucko.spark.common.sampler.async.jfr.JfrReader; +import me.lucko.spark.common.sampler.dumper.SpecificThreadDumper; import me.lucko.spark.common.sampler.node.MergeMode; import me.lucko.spark.common.sampler.node.ThreadNode; import me.lucko.spark.common.util.ClassSourceLookup; @@ -119,7 +120,7 @@ public void start() { } String command = "start,event=cpu,interval=" + this.interval + "us,threads,jfr,file=" + this.outputFile.toString(); - if (this.threadDumper instanceof ThreadDumper.Specific) { + if (this.threadDumper instanceof SpecificThreadDumper) { command += ",filter"; } @@ -128,8 +129,8 @@ public void start() { throw new RuntimeException("Unexpected response: " + resp); } - if (this.threadDumper instanceof ThreadDumper.Specific) { - ThreadDumper.Specific threadDumper = (ThreadDumper.Specific) this.threadDumper; + if (this.threadDumper instanceof SpecificThreadDumper) { + SpecificThreadDumper threadDumper = (SpecificThreadDumper) this.threadDumper; for (Thread thread : threadDumper.getThreads()) { this.profiler.addThread(thread); } @@ -187,8 +188,8 @@ private void aggregateOutput() { this.outputComplete = true; Predicate threadFilter; - if (this.threadDumper instanceof ThreadDumper.Specific) { - ThreadDumper.Specific threadDumper = (ThreadDumper.Specific) this.threadDumper; + if (this.threadDumper instanceof SpecificThreadDumper) { + SpecificThreadDumper threadDumper = (SpecificThreadDumper) this.threadDumper; threadFilter = n -> threadDumper.getThreadNames().contains(n.toLowerCase()); } else { threadFilter = n -> true; diff --git a/spark-common/src/main/java/me/lucko/spark/common/sampler/dumper/PatternThreadDumper.java b/spark-common/src/main/java/me/lucko/spark/common/sampler/dumper/PatternThreadDumper.java new file mode 100644 index 00000000..3b85bd59 --- /dev/null +++ b/spark-common/src/main/java/me/lucko/spark/common/sampler/dumper/PatternThreadDumper.java @@ -0,0 +1,69 @@ +package me.lucko.spark.common.sampler.dumper; + +import me.lucko.spark.common.sampler.ThreadDumper; +import me.lucko.spark.common.util.ThreadFinder; +import me.lucko.spark.proto.SparkProtos; + +import java.lang.management.ThreadInfo; +import java.lang.management.ThreadMXBean; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; +import java.util.stream.Collectors; + +/** + * Implementation of {@link ThreadDumper} that generates data for a regex matched set of threads. + */ +public final class PatternThreadDumper implements ThreadDumper { + + private final ThreadFinder threadFinder = new ThreadFinder(); + private final Set namePatterns; + private final Map cache = new HashMap<>(); + + public PatternThreadDumper(Set namePatterns) { + this.namePatterns = namePatterns.stream() + .map(regex -> { + try { + return Pattern.compile(regex, Pattern.CASE_INSENSITIVE); + } catch (PatternSyntaxException e) { + return null; + } + }) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + } + + @Override + public ThreadInfo[] dumpThreads(ThreadMXBean threadBean) { + return this.threadFinder.getThreads() + .filter(thread -> { + Boolean result = this.cache.get(thread.getId()); + if (result != null) { + return result; + } + + for (Pattern pattern : this.namePatterns) { + if (pattern.matcher(thread.getName()).matches()) { + this.cache.put(thread.getId(), true); + return true; + } + } + this.cache.put(thread.getId(), false); + return false; + }) + .map(thread -> threadBean.getThreadInfo(thread.getId(), Integer.MAX_VALUE)) + .filter(Objects::nonNull) + .toArray(ThreadInfo[]::new); + } + + @Override + public SparkProtos.SamplerMetadata.ThreadDumper getMetadata() { + return SparkProtos.SamplerMetadata.ThreadDumper.newBuilder() + .setType(SparkProtos.SamplerMetadata.ThreadDumper.Type.REGEX) + .addAllPatterns(this.namePatterns.stream().map(Pattern::pattern).collect(Collectors.toList())) + .build(); + } +} diff --git a/spark-common/src/main/java/me/lucko/spark/common/sampler/dumper/SpecificThreadDumper.java b/spark-common/src/main/java/me/lucko/spark/common/sampler/dumper/SpecificThreadDumper.java new file mode 100644 index 00000000..5ccce9b9 --- /dev/null +++ b/spark-common/src/main/java/me/lucko/spark/common/sampler/dumper/SpecificThreadDumper.java @@ -0,0 +1,64 @@ +package me.lucko.spark.common.sampler.dumper; + +import me.lucko.spark.common.sampler.ThreadDumper; +import me.lucko.spark.common.util.ThreadFinder; +import me.lucko.spark.proto.SparkProtos; + +import java.lang.management.ThreadInfo; +import java.lang.management.ThreadMXBean; +import java.util.Arrays; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Implementation of {@link ThreadDumper} that generates data for a specific set of threads. + */ +public final class SpecificThreadDumper implements ThreadDumper { + private final long[] ids; + private Set threads; + private Set threadNamesLowerCase; + + public SpecificThreadDumper(long[] ids) { + this.ids = ids; + } + + public SpecificThreadDumper(Set names) { + this.threadNamesLowerCase = names.stream().map(String::toLowerCase).collect(Collectors.toSet()); + this.ids = new ThreadFinder().getThreads() + .filter(t -> this.threadNamesLowerCase.contains(t.getName().toLowerCase())) + .mapToLong(Thread::getId) + .toArray(); + Arrays.sort(this.ids); + } + + public Set getThreads() { + if (this.threads == null) { + this.threads = new ThreadFinder().getThreads() + .filter(t -> Arrays.binarySearch(this.ids, t.getId()) >= 0) + .collect(Collectors.toSet()); + } + return this.threads; + } + + public Set getThreadNames() { + if (this.threadNamesLowerCase == null) { + this.threadNamesLowerCase = getThreads().stream() + .map(t -> t.getName().toLowerCase()) + .collect(Collectors.toSet()); + } + return this.threadNamesLowerCase; + } + + @Override + public ThreadInfo[] dumpThreads(ThreadMXBean threadBean) { + return threadBean.getThreadInfo(this.ids, Integer.MAX_VALUE); + } + + @Override + public SparkProtos.SamplerMetadata.ThreadDumper getMetadata() { + return SparkProtos.SamplerMetadata.ThreadDumper.newBuilder() + .setType(SparkProtos.SamplerMetadata.ThreadDumper.Type.SPECIFIC) + .addAllIds(Arrays.stream(this.ids).boxed().collect(Collectors.toList())) + .build(); + } +} diff --git a/spark-common/src/main/java/me/lucko/spark/common/sampler/grouper/NameThreadGrouper.java b/spark-common/src/main/java/me/lucko/spark/common/sampler/grouper/NameThreadGrouper.java new file mode 100644 index 00000000..3029a0c8 --- /dev/null +++ b/spark-common/src/main/java/me/lucko/spark/common/sampler/grouper/NameThreadGrouper.java @@ -0,0 +1,19 @@ +package me.lucko.spark.common.sampler.grouper; + +import me.lucko.spark.common.sampler.ThreadGrouper; +import me.lucko.spark.proto.SparkProtos; + +/** + * Implementation of {@link ThreadGrouper} that just groups by thread name. + */ +public class NameThreadGrouper implements ThreadGrouper { + @Override + public String getGroup(long threadId, String threadName) { + return threadName; + } + + @Override + public SparkProtos.SamplerMetadata.DataAggregator.ThreadGrouper asProto() { + return SparkProtos.SamplerMetadata.DataAggregator.ThreadGrouper.BY_NAME; + } +} diff --git a/spark-common/src/main/java/me/lucko/spark/common/sampler/grouper/PoolThreadGrouper.java b/spark-common/src/main/java/me/lucko/spark/common/sampler/grouper/PoolThreadGrouper.java new file mode 100644 index 00000000..0d914839 --- /dev/null +++ b/spark-common/src/main/java/me/lucko/spark/common/sampler/grouper/PoolThreadGrouper.java @@ -0,0 +1,44 @@ +package me.lucko.spark.common.sampler.grouper; + +import me.lucko.spark.common.sampler.ThreadGrouper; +import me.lucko.spark.proto.SparkProtos; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Implementation of {@link ThreadGrouper} that attempts to group by the name of the pool + * the thread originated from. + * + *

The regex pattern used to match pools expects a digit at the end of the thread name, + * separated from the pool name with any of one or more of ' ', '-', or '#'.

+ */ +public class PoolThreadGrouper implements ThreadGrouper { + + private final Map cache = new ConcurrentHashMap<>(); + private final Pattern pattern = Pattern.compile("^(.*?)[-# ]+\\d+$"); + + @Override + public String getGroup(long threadId, String threadName) { + String group = this.cache.get(threadId); + if (group != null) { + return group; + } + + Matcher matcher = this.pattern.matcher(threadName); + if (!matcher.matches()) { + return threadName; + } + + group = matcher.group(1).trim() + " (Combined)"; + this.cache.put(threadId, group); // we don't care about race conditions here + return group; + } + + @Override + public SparkProtos.SamplerMetadata.DataAggregator.ThreadGrouper asProto() { + return SparkProtos.SamplerMetadata.DataAggregator.ThreadGrouper.BY_POOL; + } +} diff --git a/spark-common/src/main/java/me/lucko/spark/common/sampler/grouper/SingleThreadGrouper.java b/spark-common/src/main/java/me/lucko/spark/common/sampler/grouper/SingleThreadGrouper.java new file mode 100644 index 00000000..b6d77d5f --- /dev/null +++ b/spark-common/src/main/java/me/lucko/spark/common/sampler/grouper/SingleThreadGrouper.java @@ -0,0 +1,21 @@ +package me.lucko.spark.common.sampler.grouper; + +import me.lucko.spark.common.sampler.ThreadGrouper; +import me.lucko.spark.proto.SparkProtos; + +/** + * Implementation of {@link ThreadGrouper} which groups all threads as one, under + * the name "All". + */ +public class SingleThreadGrouper implements ThreadGrouper { + + @Override + public String getGroup(long threadId, String threadName) { + return "All"; + } + + @Override + public SparkProtos.SamplerMetadata.DataAggregator.ThreadGrouper asProto() { + return SparkProtos.SamplerMetadata.DataAggregator.ThreadGrouper.AS_ONE; + } +} From 8a7b0b8c1df559f5cd40258572e2b3ed0d6cdb4e Mon Sep 17 00:00:00 2001 From: Bram Ceulemans Date: Thu, 19 Aug 2021 18:41:07 +0200 Subject: [PATCH 2/4] Add viewer URL to ProfilerReport --- .../main/java/me/lucko/spark/api/profiler/DumperChoice.java | 3 +++ .../main/java/me/lucko/spark/api/profiler/ProfilerReport.java | 2 ++ 2 files changed, 5 insertions(+) diff --git a/spark-api/src/main/java/me/lucko/spark/api/profiler/DumperChoice.java b/spark-api/src/main/java/me/lucko/spark/api/profiler/DumperChoice.java index 93b18029..a91eda2d 100644 --- a/spark-api/src/main/java/me/lucko/spark/api/profiler/DumperChoice.java +++ b/spark-api/src/main/java/me/lucko/spark/api/profiler/DumperChoice.java @@ -1,4 +1,7 @@ package me.lucko.spark.api.profiler; +/** + * TODO: implement this class, see {@link ProfilerConfiguration} for the purpose. + */ public final class DumperChoice { } diff --git a/spark-api/src/main/java/me/lucko/spark/api/profiler/ProfilerReport.java b/spark-api/src/main/java/me/lucko/spark/api/profiler/ProfilerReport.java index 154513c6..05853818 100644 --- a/spark-api/src/main/java/me/lucko/spark/api/profiler/ProfilerReport.java +++ b/spark-api/src/main/java/me/lucko/spark/api/profiler/ProfilerReport.java @@ -1,4 +1,6 @@ package me.lucko.spark.api.profiler; public interface ProfilerReport { + + String viewer(); } From 8a9db93a892b671c47614bba4cb5ef047866377f Mon Sep 17 00:00:00 2001 From: Bram Ceulemans Date: Thu, 19 Aug 2021 18:42:37 +0200 Subject: [PATCH 3/4] Add JavaDoc to Profiler API --- .../me/lucko/spark/api/profiler/Profiler.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/spark-api/src/main/java/me/lucko/spark/api/profiler/Profiler.java b/spark-api/src/main/java/me/lucko/spark/api/profiler/Profiler.java index 995f1027..dfa1f962 100644 --- a/spark-api/src/main/java/me/lucko/spark/api/profiler/Profiler.java +++ b/spark-api/src/main/java/me/lucko/spark/api/profiler/Profiler.java @@ -1,8 +1,23 @@ package me.lucko.spark.api.profiler; +import org.jetbrains.annotations.Nullable; + public interface Profiler { + /** + * Start the Spark Profiler using a profiler configuration. + * A configuration can be built using {@link ProfilerConfiguration#builder()}. + * + * @param configuration the configuration object + * @return if the profiler started successfully + */ boolean start(ProfilerConfiguration configuration); + /** + * Stop the currently running profiler. + * + * @return the profiler report or null if no profiler was running. + */ + @Nullable ProfilerReport stop(); } From a07d233088ab6e9ce5176fd7ba99c922980f78d9 Mon Sep 17 00:00:00 2001 From: Bram Ceulemans Date: Thu, 19 Aug 2021 23:44:57 +0200 Subject: [PATCH 4/4] WIP SamplerService --- spark-api/build.gradle | 4 +- .../me/lucko/spark/api/profiler/Dumper.java | 62 ++++++++++++++ .../spark/api/profiler/DumperChoice.java | 7 -- .../api/profiler/ProfilerConfiguration.java | 3 +- .../ProfilerConfigurationBuilder.java | 21 ++++- spark-common/build.gradle | 1 + .../me/lucko/spark/common/SparkPlatform.java | 35 ++++---- .../spark/common/sampler/SamplerService.java | 83 +++++++++++++++++++ .../spark/common/sampler/ThreadGrouper.java | 6 +- 9 files changed, 186 insertions(+), 36 deletions(-) create mode 100644 spark-api/src/main/java/me/lucko/spark/api/profiler/Dumper.java delete mode 100644 spark-api/src/main/java/me/lucko/spark/api/profiler/DumperChoice.java create mode 100644 spark-common/src/main/java/me/lucko/spark/common/sampler/SamplerService.java diff --git a/spark-api/build.gradle b/spark-api/build.gradle index 31458af6..464a763e 100644 --- a/spark-api/build.gradle +++ b/spark-api/build.gradle @@ -5,8 +5,8 @@ plugins { version = '0.1-SNAPSHOT' dependencies { - compileOnly 'org.checkerframework:checker-qual:3.8.0' - compileOnly 'org.jetbrains:annotations:20.1.0' + compileOnly 'org.checkerframework:checker-qual:3.16.0' + compileOnly 'org.jetbrains:annotations:22.0.0' } publishing { diff --git a/spark-api/src/main/java/me/lucko/spark/api/profiler/Dumper.java b/spark-api/src/main/java/me/lucko/spark/api/profiler/Dumper.java new file mode 100644 index 00000000..8a4615a6 --- /dev/null +++ b/spark-api/src/main/java/me/lucko/spark/api/profiler/Dumper.java @@ -0,0 +1,62 @@ +package me.lucko.spark.api.profiler; + +import java.util.Objects; +import java.util.Set; + +/** + * TODO: implement this class, see {@link ProfilerConfiguration} for the purpose. + */ +public final class Dumper { + private final Method method; + private final Set criteria; + + /** + * + */ + public Dumper(Method method, Set criteria) { + this.method = method; + this.criteria = criteria; + } + + public static Dumper pattern(Set criteria) { + return new Dumper(Method.PATTERN, criteria); + } + + public static Dumper specific(Set criteria) { + return new Dumper(Method.SPECIFIC, criteria); + } + + public Method method() { + return method; + } + + public Set criteria() { + return criteria; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) return true; + if (obj == null || obj.getClass() != this.getClass()) return false; + Dumper that = (Dumper) obj; + return Objects.equals(this.method, that.method) && + Objects.equals(this.criteria, that.criteria); + } + + @Override + public int hashCode() { + return Objects.hash(method, criteria); + } + + @Override + public String toString() { + return "Dumper[" + + "method=" + method + ", " + + "criteria=" + criteria + ']'; + } + + public enum Method { + PATTERN, + SPECIFIC + } +} diff --git a/spark-api/src/main/java/me/lucko/spark/api/profiler/DumperChoice.java b/spark-api/src/main/java/me/lucko/spark/api/profiler/DumperChoice.java deleted file mode 100644 index a91eda2d..00000000 --- a/spark-api/src/main/java/me/lucko/spark/api/profiler/DumperChoice.java +++ /dev/null @@ -1,7 +0,0 @@ -package me.lucko.spark.api.profiler; - -/** - * TODO: implement this class, see {@link ProfilerConfiguration} for the purpose. - */ -public final class DumperChoice { -} diff --git a/spark-api/src/main/java/me/lucko/spark/api/profiler/ProfilerConfiguration.java b/spark-api/src/main/java/me/lucko/spark/api/profiler/ProfilerConfiguration.java index 7fe4b582..b0c1b5b3 100644 --- a/spark-api/src/main/java/me/lucko/spark/api/profiler/ProfilerConfiguration.java +++ b/spark-api/src/main/java/me/lucko/spark/api/profiler/ProfilerConfiguration.java @@ -40,6 +40,7 @@ static ProfilerConfigurationBuilder builder() { /** * Minimum duration (in millis) a tick has to take in order to be recorded. + * If this value is below 0, all ticks will be recorded. * * @return the minimum tick duration */ @@ -60,7 +61,7 @@ static ProfilerConfigurationBuilder builder() { * @return the thread dumper choice */ @Nullable - DumperChoice dumper(); + Dumper dumper(); /** * Get the choice of which thread grouper (AS_ONE, BY_NAME, BY_POOL) to use for this profiler. diff --git a/spark-api/src/main/java/me/lucko/spark/api/profiler/ProfilerConfigurationBuilder.java b/spark-api/src/main/java/me/lucko/spark/api/profiler/ProfilerConfigurationBuilder.java index 0b3dec52..2d4e1c02 100644 --- a/spark-api/src/main/java/me/lucko/spark/api/profiler/ProfilerConfigurationBuilder.java +++ b/spark-api/src/main/java/me/lucko/spark/api/profiler/ProfilerConfigurationBuilder.java @@ -12,11 +12,17 @@ public class ProfilerConfigurationBuilder { private boolean forceJavaSampler = true; private int minimumTickDuration = -1; private @Nullable Duration duration = null; - private @Nullable DumperChoice dumper = null; + private @Nullable Dumper dumper = null; private @Nullable GrouperChoice grouper = null; + /** + * Set the interval to a given value or 5 if value is below 0. + * + * @param interval the interval + * @return the builder instance + */ public ProfilerConfigurationBuilder interval(double interval) { - this.interval = interval; + this.interval = interval > 0 ? interval : 5; return this; } @@ -35,6 +41,13 @@ public ProfilerConfigurationBuilder forceJavaSampler(boolean forceJavaSampler) { return this; } + /** + * Set the minimum tick duration that will be profiled. + * If the minimumTickDuration is lower than 0 (default is -1), all ticks will be recorded. + * + * @param minimumTickDuration the minimum tick duration + * @return the builder instance + */ public ProfilerConfigurationBuilder minimumTickDuration(int minimumTickDuration) { this.minimumTickDuration = minimumTickDuration; return this; @@ -45,7 +58,7 @@ public ProfilerConfigurationBuilder duration(Duration duration) { return this; } - public ProfilerConfigurationBuilder dumper(DumperChoice dumper) { + public ProfilerConfigurationBuilder dumper(Dumper dumper) { this.dumper = dumper; return this; } @@ -88,7 +101,7 @@ public int minimumTickDuration() { } @Override - public @Nullable DumperChoice dumper() { + public @Nullable Dumper dumper() { return dumper; } diff --git a/spark-common/build.gradle b/spark-common/build.gradle index 1243fd89..aa06d09b 100644 --- a/spark-common/build.gradle +++ b/spark-common/build.gradle @@ -28,6 +28,7 @@ dependencies { compileOnly 'com.google.code.gson:gson:2.7' compileOnly 'com.google.guava:guava:19.0' compileOnly 'org.checkerframework:checker-qual:3.8.0' + compileOnly 'org.jetbrains:annotations:22.0.0' } processResources { 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 38fc715c..78ed57fe 100644 --- a/spark-common/src/main/java/me/lucko/spark/common/SparkPlatform.java +++ b/spark-common/src/main/java/me/lucko/spark/common/SparkPlatform.java @@ -22,33 +22,26 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; - import me.lucko.spark.common.activitylog.ActivityLog; import me.lucko.spark.common.api.SparkApi; import me.lucko.spark.common.command.Arguments; import me.lucko.spark.common.command.Command; import me.lucko.spark.common.command.CommandModule; import me.lucko.spark.common.command.CommandResponseHandler; -import me.lucko.spark.common.command.modules.ActivityLogModule; -import me.lucko.spark.common.command.modules.GcMonitoringModule; -import me.lucko.spark.common.command.modules.HealthModule; -import me.lucko.spark.common.command.modules.HeapAnalysisModule; -import me.lucko.spark.common.command.modules.SamplerModule; -import me.lucko.spark.common.command.modules.TickMonitoringModule; +import me.lucko.spark.common.command.modules.*; import me.lucko.spark.common.command.sender.CommandSender; import me.lucko.spark.common.command.tabcomplete.CompletionSupplier; import me.lucko.spark.common.command.tabcomplete.TabCompleter; import me.lucko.spark.common.monitor.cpu.CpuMonitor; import me.lucko.spark.common.monitor.memory.GarbageCollectorStatistics; import me.lucko.spark.common.monitor.tick.TickStatistics; +import me.lucko.spark.common.sampler.SamplerService; import me.lucko.spark.common.tick.TickHook; import me.lucko.spark.common.tick.TickReporter; import me.lucko.spark.common.util.BytebinClient; import me.lucko.spark.common.util.ClassSourceLookup; import me.lucko.spark.common.util.Configuration; - import net.kyori.adventure.text.event.ClickEvent; - import okhttp3.OkHttpClient; import java.io.IOException; @@ -56,22 +49,14 @@ import java.nio.file.Path; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.locks.ReentrantLock; 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; -import static net.kyori.adventure.text.format.NamedTextColor.WHITE; +import static net.kyori.adventure.text.format.NamedTextColor.*; import static net.kyori.adventure.text.format.TextDecoration.BOLD; import static net.kyori.adventure.text.format.TextDecoration.UNDERLINED; @@ -80,7 +65,9 @@ */ public class SparkPlatform { - /** The date time formatter instance used by the platform */ + /** + * The date time formatter instance used by the platform + */ private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH.mm.ss"); private final SparkPlugin plugin; @@ -94,11 +81,12 @@ public class SparkPlatform { private final ActivityLog activityLog; private final TickHook tickHook; private final TickReporter tickReporter; + private final SamplerService samplerService; private final ClassSourceLookup classSourceLookup; private final TickStatistics tickStatistics; + private final AtomicBoolean enabled = new AtomicBoolean(false); private Map startupGcStatistics = ImmutableMap.of(); private long serverNormalOperationStartTime; - private final AtomicBoolean enabled = new AtomicBoolean(false); public SparkPlatform(SparkPlugin plugin) { this.plugin = plugin; @@ -131,6 +119,7 @@ public SparkPlatform(SparkPlugin plugin) { this.tickHook = plugin.createTickHook(); this.tickReporter = plugin.createTickReporter(); + this.samplerService = new SamplerService(this); this.classSourceLookup = plugin.createClassSourceLookup(); this.tickStatistics = this.tickHook != null ? new TickStatistics() : null; } @@ -209,6 +198,10 @@ public TickReporter getTickReporter() { return this.tickReporter; } + public SamplerService getSamplerService() { + return this.samplerService; + } + public ClassSourceLookup getClassSourceLookup() { return this.classSourceLookup; } diff --git a/spark-common/src/main/java/me/lucko/spark/common/sampler/SamplerService.java b/spark-common/src/main/java/me/lucko/spark/common/sampler/SamplerService.java new file mode 100644 index 00000000..50677a2d --- /dev/null +++ b/spark-common/src/main/java/me/lucko/spark/common/sampler/SamplerService.java @@ -0,0 +1,83 @@ +package me.lucko.spark.common.sampler; + +import me.lucko.spark.api.profiler.Dumper; +import me.lucko.spark.api.profiler.Profiler; +import me.lucko.spark.api.profiler.ProfilerConfiguration; +import me.lucko.spark.api.profiler.ProfilerReport; +import me.lucko.spark.common.SparkPlatform; +import me.lucko.spark.common.sampler.dumper.PatternThreadDumper; +import me.lucko.spark.common.sampler.dumper.SpecificThreadDumper; +import me.lucko.spark.common.tick.TickHook; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.time.Duration; +import java.util.Set; + +public class SamplerService implements Profiler { + + public static final int MINIMUM_DURATION = 10; + public static final String THREAD_WILDCARD = "*"; + + private final SparkPlatform platform; + + private Sampler active = null; + + public SamplerService(SparkPlatform platform) { + this.platform = platform; + } + + @Override + public boolean start(ProfilerConfiguration configuration) { + if (active != null) { + return false; + } + + Duration duration = configuration.duration(); + if (duration != null && duration.getSeconds() <= MINIMUM_DURATION) { + return false; + } + + double interval = configuration.interval(); + if (interval <= 0) { + return false; + } + + ThreadDumper dumper = getThreadDumper(configuration.dumper()); + ThreadGrouper grouper = ThreadGrouper.get(configuration.grouper()); + + int minimum = configuration.minimumTickDuration(); + if (minimum >= 0) { + TickHook hook = platform.getTickHook(); + if (hook == null) { + return false; + } + } + + return true; + } + + @Override + public @Nullable ProfilerReport stop() { + return null; + } + + @NotNull + private ThreadDumper getThreadDumper(@Nullable Dumper dumper) { + if (dumper == null) { + return platform.getPlugin().getDefaultThreadDumper(); + } + + Set criteria = dumper.criteria(); + if (criteria.contains(THREAD_WILDCARD)) { + return ThreadDumper.ALL; + } + + Dumper.Method method = dumper.method(); + if (Dumper.Method.PATTERN.equals(method)) { + return new PatternThreadDumper(criteria); + } + + return new SpecificThreadDumper(criteria); + } +} diff --git a/spark-common/src/main/java/me/lucko/spark/common/sampler/ThreadGrouper.java b/spark-common/src/main/java/me/lucko/spark/common/sampler/ThreadGrouper.java index aa7f2c6f..84c0f383 100644 --- a/spark-common/src/main/java/me/lucko/spark/common/sampler/ThreadGrouper.java +++ b/spark-common/src/main/java/me/lucko/spark/common/sampler/ThreadGrouper.java @@ -25,6 +25,7 @@ import me.lucko.spark.common.sampler.grouper.PoolThreadGrouper; import me.lucko.spark.common.sampler.grouper.SingleThreadGrouper; import me.lucko.spark.proto.SparkProtos.SamplerMetadata; +import org.jetbrains.annotations.Nullable; /** * Function for grouping threads together @@ -35,7 +36,10 @@ public interface ThreadGrouper { ThreadGrouper BY_POOL = new PoolThreadGrouper(); ThreadGrouper AS_ONE = new SingleThreadGrouper(); - static ThreadGrouper get(GrouperChoice choice) { + static ThreadGrouper get(@Nullable GrouperChoice choice) { + if (choice == null) { + return BY_POOL; + } switch (choice) { case SINGLE: return AS_ONE;