diff options
| author | lucko <git@lucko.me> | 2021-03-10 15:44:14 +0000 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2021-03-10 15:44:14 +0000 |
| commit | 9ee26a12f12a89a6df14eb86afce5d88b5a2cf25 (patch) | |
| tree | 840ab480ae324b50cefc63b69dae9b38660dc94f | |
| parent | fd6736fc2f0bae48dda1d4b595e867d9c7244c27 (diff) | |
| download | spark-9ee26a12f12a89a6df14eb86afce5d88b5a2cf25.tar.gz spark-9ee26a12f12a89a6df14eb86afce5d88b5a2cf25.tar.bz2 spark-9ee26a12f12a89a6df14eb86afce5d88b5a2cf25.zip | |
async-profiler support (#102)
37 files changed, 1810 insertions, 260 deletions
diff --git a/build.gradle b/build.gradle index 8ea1928..ad44d34 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ allprojects { group = 'me.lucko' - version = '1.4-SNAPSHOT' + version = '1.5-SNAPSHOT' configurations { compileClasspath // Fabric-loom needs this for remap jar for some reason @@ -13,7 +13,7 @@ subprojects { apply plugin: 'idea' ext { - pluginVersion = '1.4.3' + pluginVersion = '1.5.0' pluginDescription = 'spark is a performance profiling plugin based on sk89q\'s WarmRoast profiler' } @@ -28,6 +28,7 @@ subprojects { mavenCentral() maven { url "https://oss.sonatype.org/content/repositories/snapshots/" } maven { url "https://repo.lucko.me/" } + maven { url "https://jitpack.io" } } } diff --git a/spark-common/build.gradle b/spark-common/build.gradle index 3f113dd..8b64449 100644 --- a/spark-common/build.gradle +++ b/spark-common/build.gradle @@ -3,19 +3,20 @@ plugins { } dependencies { + compile 'com.github.jvm-profiling-tools:async-profiler:v2.0-rc' compile 'org.ow2.asm:asm:7.1' compile 'com.google.protobuf:protobuf-java:3.14.0' compile 'com.squareup.okhttp3:okhttp:3.14.1' compile 'com.squareup.okio:okio:1.17.3' compile 'org.tukaani:xz:1.8' - compile('net.kyori:adventure-api:4.1.1') { + compile('net.kyori:adventure-api:4.7.0') { exclude(module: 'checker-qual') } - compile('net.kyori:adventure-text-serializer-gson:4.1.1') { + compile('net.kyori:adventure-text-serializer-gson:4.7.0') { exclude(module: 'adventure-api') exclude(module: 'gson') } - compile('net.kyori:adventure-text-serializer-legacy:4.1.1') { + compile('net.kyori:adventure-text-serializer-legacy:4.7.0') { exclude(module: 'adventure-api') } compile('net.kyori:adventure-text-feature-pagination:4.0.0-SNAPSHOT') { @@ -25,6 +26,12 @@ dependencies { compileOnly 'com.google.guava:guava:19.0' } +processResources { + from(sourceSets.main.resources.srcDirs) { + include 'libasyncProfiler.so' + } +} + protobuf { protoc { artifact = 'com.google.protobuf:protoc:3.14.0' 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 cf3b0ee..a6bf332 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 @@ -211,7 +211,7 @@ public class SparkPlatform { if (command.aliases().contains(alias)) { try { command.executor().execute(this, sender, resp, new Arguments(rawArgs)); - } catch (IllegalArgumentException e) { + } catch (Arguments.ParseException e) { resp.replyPrefixed(text(e.getMessage(), RED)); } return; 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 2b202af..3cd0365 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 @@ -51,7 +51,7 @@ public class Arguments { if (flag == null || matches) { if (!matches) { - throw new IllegalArgumentException("Expected flag at position " + i + " but got '" + arg + "' instead!"); + throw new ParseException("Expected flag at position " + i + " but got '" + arg + "' instead!"); } // store existing value, if present @@ -83,7 +83,7 @@ public class Arguments { try { return Math.abs(Integer.parseInt(it.next())); } catch (NumberFormatException e) { - throw new IllegalArgumentException("Invalid input for '" + key + "' argument. Please specify a number!"); + throw new ParseException("Invalid input for '" + key + "' argument. Please specify a number!"); } } return -1; // undefined @@ -95,7 +95,7 @@ public class Arguments { try { return Math.abs(Double.parseDouble(it.next())); } catch (NumberFormatException e) { - throw new IllegalArgumentException("Invalid input for '" + key + "' argument. Please specify a number!"); + throw new ParseException("Invalid input for '" + key + "' argument. Please specify a number!"); } } return -1; // undefined @@ -108,4 +108,21 @@ public class Arguments { public boolean boolFlag(String key) { return this.parsedArgs.containsKey(key); } + + public static final class ParseException extends IllegalArgumentException { + public ParseException() { + } + + public ParseException(String s) { + super(s); + } + + public ParseException(String message, Throwable cause) { + super(message, cause); + } + + public ParseException(Throwable cause) { + super(cause); + } + } } diff --git a/spark-common/src/main/java/me/lucko/spark/common/command/modules/SamplerModule.java b/spark-common/src/main/java/me/lucko/spark/common/command/modules/SamplerModule.java index eb77b24..cce3169 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 @@ -33,6 +33,7 @@ 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.async.AsyncSampler; import me.lucko.spark.common.sampler.node.MergeMode; import me.lucko.spark.common.sampler.tick.TickHook; import me.lucko.spark.common.util.MethodDisambiguator; @@ -55,13 +56,13 @@ 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 WarmRoast instance currently running, if any */ + /** The sampler instance currently running, if any */ private Sampler activeSampler = null; @Override public void close() { if (this.activeSampler != null) { - this.activeSampler.cancel(); + this.activeSampler.stop(); this.activeSampler = null; } } @@ -83,6 +84,7 @@ public class SamplerModule implements CommandModule { .argumentUsage("only-ticks-over", "tick length millis") .argumentUsage("ignore-sleeping", null) .argumentUsage("ignore-native", null) + .argumentUsage("force-java-sampler", null) .argumentUsage("order-by-time", null) .argumentUsage("separate-parent-calls", null) .executor((platform, sender, resp, arguments) -> { @@ -118,7 +120,7 @@ public class SamplerModule implements CommandModule { if (this.activeSampler == null) { resp.replyPrefixed(text("There isn't an active sampling task running.")); } else { - this.activeSampler.cancel(); + this.activeSampler.stop(); resp.broadcastPrefixed(text("The active sampling operation has been stopped! Uploading results...")); ThreadNodeOrder threadOrder = arguments.boolFlag("order-by-time") ? ThreadNodeOrder.BY_TIME : ThreadNodeOrder.BY_NAME; String comment = Iterables.getFirst(arguments.stringFlag("comment"), null); @@ -149,6 +151,7 @@ public class SamplerModule implements CommandModule { boolean ignoreSleeping = arguments.boolFlag("ignore-sleeping"); boolean ignoreNative = arguments.boolFlag("ignore-native"); + boolean forceJavaSampler = arguments.boolFlag("force-java-sampler"); Set<String> threads = arguments.stringFlag("thread"); ThreadDumper threadDumper; @@ -201,19 +204,25 @@ public class SamplerModule implements CommandModule { builder.samplingInterval(intervalMillis); builder.ignoreSleeping(ignoreSleeping); builder.ignoreNative(ignoreNative); + builder.forceJavaSampler(forceJavaSampler); if (ticksOver != -1) { builder.ticksOver(ticksOver, tickHook); } Sampler sampler = this.activeSampler = builder.start(); - resp.broadcastPrefixed(text("Profiler now active!", GOLD)); + resp.broadcastPrefixed(text() + .append(text("Profiler now active!", GOLD)) + .append(space()) + .append(text("(" + (sampler instanceof AsyncSampler ? "async" : "built-in java") + ")", DARK_GRAY)) + .build() + ); if (timeoutSeconds == -1) { resp.broadcastPrefixed(text("Use '/" + platform.getPlugin().getCommandName() + " profiler --stop' to stop profiling and upload the results.")); } else { resp.broadcastPrefixed(text("The results will be automatically returned after the profiler has been running for " + timeoutSeconds + " seconds.")); } - CompletableFuture<Sampler> future = this.activeSampler.getFuture(); + CompletableFuture<? extends Sampler> future = this.activeSampler.getFuture(); // send message if profiling fails future.whenCompleteAsync((s, throwable) -> { @@ -253,8 +262,8 @@ public class SamplerModule implements CommandModule { List<String> opts = new ArrayList<>(Arrays.asList("--info", "--stop", "--cancel", "--timeout", "--regex", "--combine-all", "--not-combined", "--interval", - "--only-ticks-over", "--ignore-sleeping", "--ignore-native", "--order-by-time", - "--separate-parent-calls", "--comment")); + "--only-ticks-over", "--ignore-sleeping", "--ignore-native", "--force-java-sampler", + "--order-by-time", "--separate-parent-calls", "--comment")); opts.removeAll(arguments); opts.add("--thread"); // allowed multiple times 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 e772cb3..5088ed7 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 @@ -1,7 +1,6 @@ /* * This file is part of spark. * - * Copyright (C) Albert Pham <http://www.sk89q.com> * Copyright (c) lucko (Luck) <luck@lucko.me> * Copyright (c) contributors * @@ -21,174 +20,66 @@ package me.lucko.spark.common.sampler; -import com.google.common.util.concurrent.ThreadFactoryBuilder; import me.lucko.spark.common.command.sender.CommandSender; import me.lucko.spark.common.platform.PlatformInfo; -import me.lucko.spark.common.sampler.aggregator.DataAggregator; -import me.lucko.spark.common.sampler.aggregator.SimpleDataAggregator; -import me.lucko.spark.common.sampler.aggregator.TickedDataAggregator; import me.lucko.spark.common.sampler.node.MergeMode; import me.lucko.spark.common.sampler.node.ThreadNode; -import me.lucko.spark.common.sampler.tick.TickHook; import me.lucko.spark.proto.SparkProtos.SamplerData; -import me.lucko.spark.proto.SparkProtos.SamplerMetadata; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; -import java.lang.management.ManagementFactory; -import java.lang.management.ThreadInfo; -import java.lang.management.ThreadMXBean; -import java.util.ArrayList; import java.util.Comparator; -import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; import java.util.zip.GZIPOutputStream; /** - * Main sampler class. + * Abstract superinterface for all sampler implementations. */ -public class Sampler implements Runnable { - private static final AtomicInteger THREAD_ID = new AtomicInteger(0); - - /** The worker pool for inserting stack nodes */ - private final ScheduledExecutorService workerPool = Executors.newScheduledThreadPool( - 6, new ThreadFactoryBuilder().setNameFormat("spark-worker-" + THREAD_ID.getAndIncrement() + "-%d").build() - ); - - /** The main sampling task */ - private ScheduledFuture<?> task; - - /** The thread management interface for the current JVM */ - private final ThreadMXBean threadBean = ManagementFactory.getThreadMXBean(); - /** The instance used to generate thread information for use in sampling */ - private final ThreadDumper threadDumper; - /** Responsible for aggregating and then outputting collected sampling data */ - private final DataAggregator dataAggregator; - - /** A future to encapsulation the completion of this sampler instance */ - private final CompletableFuture<Sampler> future = new CompletableFuture<>(); - - /** The interval to wait between sampling, in microseconds */ - private final int interval; - /** The time when sampling first began */ - private long startTime = -1; - /** The unix timestamp (in millis) when this sampler should automatically complete.*/ - private final long endTime; // -1 for nothing - - public Sampler(int interval, ThreadDumper threadDumper, ThreadGrouper threadGrouper, long endTime, boolean ignoreSleeping, boolean ignoreNative) { - this.threadDumper = threadDumper; - this.dataAggregator = new SimpleDataAggregator(this.workerPool, threadGrouper, interval, ignoreSleeping, ignoreNative); - this.interval = interval; - this.endTime = endTime; - } - - public Sampler(int interval, ThreadDumper threadDumper, ThreadGrouper threadGrouper, long endTime, boolean ignoreSleeping, boolean ignoreNative, TickHook tickHook, int tickLengthThreshold) { - this.threadDumper = threadDumper; - this.dataAggregator = new TickedDataAggregator(this.workerPool, threadGrouper, interval, ignoreSleeping, ignoreNative, tickHook, tickLengthThreshold); - this.interval = interval; - this.endTime = endTime; - } +public interface Sampler { /** * Starts the sampler. */ - public void start() { - this.startTime = System.currentTimeMillis(); - this.task = this.workerPool.scheduleAtFixedRate(this, 0, this.interval, TimeUnit.MICROSECONDS); - } - - public long getStartTime() { - if (this.startTime == -1) { - throw new IllegalStateException("Not yet started"); - } - return this.startTime; - } - - public long getEndTime() { - return this.endTime; - } - - public CompletableFuture<Sampler> getFuture() { - return this.future; - } + void start(); - public void cancel() { - this.task.cancel(false); - } - - @Override - public void run() { - // this is effectively synchronized, the worker pool will not allow this task - // to concurrently execute. - try { - if (this.endTime != -1 && this.endTime <= System.currentTimeMillis()) { - this.future.complete(this); - cancel(); - return; - } - - ThreadInfo[] threadDumps = this.threadDumper.dumpThreads(this.threadBean); - this.workerPool.execute(new InsertDataTask(this.dataAggregator, threadDumps)); - } catch (Throwable t) { - this.future.completeExceptionally(t); - cancel(); - } - } - - private static final class InsertDataTask implements Runnable { - private final DataAggregator dataAggregator; - private final ThreadInfo[] threadDumps; - - InsertDataTask(DataAggregator dataAggregator, ThreadInfo[] threadDumps) { - this.dataAggregator = dataAggregator; - this.threadDumps = threadDumps; - } - - @Override - public void run() { - for (ThreadInfo threadInfo : this.threadDumps) { - if (threadInfo.getThreadName() == null || threadInfo.getStackTrace() == null) { - continue; - } - this.dataAggregator.insertData(threadInfo); - } - } - } - - private SamplerData toProto(PlatformInfo platformInfo, CommandSender creator, Comparator<? super Map.Entry<String, ThreadNode>> outputOrder, String comment, MergeMode mergeMode) { - final SamplerMetadata.Builder metadata = SamplerMetadata.newBuilder() - .setPlatform(platformInfo.toData().toProto()) - .setUser(creator.toData().toProto()) - .setStartTime(this.startTime) - .setInterval(this.interval) - .setThreadDumper(this.threadDumper.getMetadata()) - .setDataAggregator(this.dataAggregator.getMetadata()); - - if (comment != null) { - metadata.setComment(comment); - } - - SamplerData.Builder proto = SamplerData.newBuilder(); - proto.setMetadata(metadata.build()); + /** + * Stops the sampler. + */ + void stop(); - List<Map.Entry<String, ThreadNode>> data = new ArrayList<>(this.dataAggregator.getData().entrySet()); - data.sort(outputOrder); + /** + * Gets the time when the sampler started (unix timestamp in millis) + * + * @return the start time + */ + long getStartTime(); - for (Map.Entry<String, ThreadNode> entry : data) { - proto.addThreads(entry.getValue().toProto(mergeMode)); - } + /** + * Gets the time when the sampler should automatically stop (unix timestamp in millis) + * + * @return the end time, or -1 if undefined + */ + long getEndTime(); - return proto.build(); - } + /** + * Gets a future to encapsulate the completion of the sampler + * + * @return a future + */ + CompletableFuture<? extends Sampler> getFuture(); + + // Methods used to export the sampler data to the web viewer. + SamplerData toProto( + PlatformInfo platformInfo, + CommandSender creator, + Comparator<? super Map.Entry<String, ThreadNode>> outputOrder, + String comment, + MergeMode mergeMode + ); - public byte[] formCompressedDataPayload(PlatformInfo platformInfo, CommandSender creator, Comparator<? super Map.Entry<String, ThreadNode>> outputOrder, String comment, MergeMode mergeMode) { + default byte[] formCompressedDataPayload(PlatformInfo platformInfo, CommandSender creator, Comparator<? super Map.Entry<String, ThreadNode>> outputOrder, String comment, MergeMode mergeMode) { SamplerData proto = toProto(platformInfo, creator, outputOrder, comment, mergeMode); ByteArrayOutputStream byteOut = new ByteArrayOutputStream(); 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 8993445..7abe1a7 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 @@ -20,6 +20,9 @@ package me.lucko.spark.common.sampler; +import me.lucko.spark.common.sampler.async.AsyncProfilerAccess; +import me.lucko.spark.common.sampler.async.AsyncSampler; +import me.lucko.spark.common.sampler.java.JavaSampler; import me.lucko.spark.common.sampler.tick.TickHook; import java.util.concurrent.TimeUnit; @@ -32,6 +35,7 @@ public class SamplerBuilder { private double samplingInterval = 4; // milliseconds private boolean ignoreSleeping = false; private boolean ignoreNative = false; + private boolean useAsyncProfiler = true; private long timeout = -1; private ThreadDumper threadDumper = ThreadDumper.ALL; private ThreadGrouper threadGrouper = ThreadGrouper.BY_NAME; @@ -81,14 +85,23 @@ public class SamplerBuilder { return this; } + public SamplerBuilder forceJavaSampler(boolean forceJavaSampler) { + this.useAsyncProfiler = !forceJavaSampler; + return this; + } + public Sampler start() { Sampler sampler; int intervalMicros = (int) (this.samplingInterval * 1000d); if (this.ticksOver == -1 || this.tickHook == null) { - sampler = new Sampler(intervalMicros, this.threadDumper, this.threadGrouper, this.timeout, this.ignoreSleeping, this.ignoreNative); + if (this.useAsyncProfiler && this.timeout == -1 && !(this.threadDumper instanceof ThreadDumper.Regex) && 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); + } } else { - sampler = new Sampler(intervalMicros, this.threadDumper, this.threadGrouper, this.timeout, this.ignoreSleeping, this.ignoreNative, this.tickHook, this.ticksOver); + sampler = new JavaSampler(intervalMicros, this.threadDumper, this.threadGrouper, this.timeout, this.ignoreSleeping, this.ignoreNative, this.tickHook, this.ticksOver); } sampler.start(); 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 224918e..4863482 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 @@ -76,17 +76,38 @@ public interface ThreadDumper { */ final class Specific implements ThreadDumper { private final long[] ids; + private Set<Thread> threads; + private Set<String> threadNamesLowerCase; public Specific(long[] ids) { this.ids = ids; } public Specific(Set<String> names) { - Set<String> namesLower = names.stream().map(String::toLowerCase).collect(Collectors.toSet()); + this.threadNamesLowerCase = names.stream().map(String::toLowerCase).collect(Collectors.toSet()); this.ids = new ThreadFinder().getThreads() - .filter(t -> namesLower.contains(t.getName().toLowerCase())) + .filter(t -> this.threadNamesLowerCase.contains(t.getName().toLowerCase())) .mapToLong(Thread::getId) .toArray(); + Arrays.sort(this.ids); + } + + public Set<Thread> 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<String> getThreadNames() { + if (this.threadNamesLowerCase == null) { + this.threadNamesLowerCase = getThreads().stream() + .map(t -> t.getName().toLowerCase()) + .collect(Collectors.toSet()); + } + return this.threadNamesLowerCase; } @Override 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 c9a4f37..7640d60 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 @@ -23,37 +23,22 @@ package me.lucko.spark.common.sampler.aggregator; import me.lucko.spark.common.sampler.ThreadGrouper; import me.lucko.spark.common.sampler.node.ThreadNode; -import java.lang.management.ThreadInfo; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ExecutorService; +/** + * Abstract implementat |
