aboutsummaryrefslogtreecommitdiff
path: root/spark-common/src/main/java/me/lucko/spark
diff options
context:
space:
mode:
Diffstat (limited to 'spark-common/src/main/java/me/lucko/spark')
-rw-r--r--spark-common/src/main/java/me/lucko/spark/common/command/modules/SamplerModule.java22
-rw-r--r--spark-common/src/main/java/me/lucko/spark/common/sampler/Sampler.java19
-rw-r--r--spark-common/src/main/java/me/lucko/spark/common/sampler/SamplerBuilder.java10
-rw-r--r--spark-common/src/main/java/me/lucko/spark/common/sampler/aggregator/AbstractDataAggregator.java8
-rw-r--r--spark-common/src/main/java/me/lucko/spark/common/sampler/aggregator/SimpleDataAggregator.java4
-rw-r--r--spark-common/src/main/java/me/lucko/spark/common/sampler/aggregator/TickedDataAggregator.java4
-rw-r--r--spark-common/src/main/java/me/lucko/spark/common/sampler/node/AbstractNode.java52
-rw-r--r--spark-common/src/main/java/me/lucko/spark/common/sampler/node/MergeMode.java90
-rw-r--r--spark-common/src/main/java/me/lucko/spark/common/sampler/node/StackTraceNode.java135
-rw-r--r--spark-common/src/main/java/me/lucko/spark/common/sampler/node/ThreadNode.java6
-rw-r--r--spark-common/src/main/java/me/lucko/spark/common/util/MethodDisambiguator.java156
11 files changed, 422 insertions, 84 deletions
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 919931e..1959a34 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
@@ -32,7 +32,9 @@ 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.node.MergeMode;
import me.lucko.spark.common.sampler.tick.TickHook;
+import me.lucko.spark.common.util.MethodDisambiguator;
import net.kyori.text.TextComponent;
import net.kyori.text.event.ClickEvent;
import net.kyori.text.format.TextColor;
@@ -76,9 +78,9 @@ public class SamplerModule implements CommandModule {
.argumentUsage("not-combined", null)
.argumentUsage("interval", "interval millis")
.argumentUsage("only-ticks-over", "tick length millis")
- .argumentUsage("include-line-numbers", null)
.argumentUsage("ignore-sleeping", null)
.argumentUsage("order-by-time", null)
+ .argumentUsage("separate-parent-calls", null)
.executor((platform, sender, resp, arguments) -> {
if (arguments.boolFlag("info")) {
if (this.activeSampler == null) {
@@ -115,7 +117,9 @@ public class SamplerModule implements CommandModule {
this.activeSampler.cancel();
resp.broadcastPrefixed(TextComponent.of("The active sampling operation has been stopped! Uploading results..."));
ThreadNodeOrder threadOrder = arguments.boolFlag("order-by-time") ? ThreadNodeOrder.BY_TIME : ThreadNodeOrder.BY_NAME;
- handleUpload(platform, resp, this.activeSampler, threadOrder);
+ MethodDisambiguator methodDisambiguator = new MethodDisambiguator();
+ MergeMode mergeMode = arguments.boolFlag("separate-parent-calls") ? MergeMode.separateParentCalls(methodDisambiguator) : MergeMode.sameMethod(methodDisambiguator);
+ handleUpload(platform, resp, this.activeSampler, threadOrder, mergeMode);
this.activeSampler = null;
}
return;
@@ -190,7 +194,6 @@ public class SamplerModule implements CommandModule {
builder.completeAfter(timeoutSeconds, TimeUnit.SECONDS);
}
builder.samplingInterval(intervalMillis);
- builder.includeLineNumbers(includeLineNumbers);
builder.ignoreSleeping(ignoreSleeping);
if (ticksOver != -1) {
builder.ticksOver(ticksOver, tickHook);
@@ -224,9 +227,11 @@ public class SamplerModule implements CommandModule {
// await the result
if (timeoutSeconds != -1) {
ThreadNodeOrder threadOrder = arguments.boolFlag("order-by-time") ? ThreadNodeOrder.BY_TIME : ThreadNodeOrder.BY_NAME;
+ MethodDisambiguator methodDisambiguator = new MethodDisambiguator();
+ MergeMode mergeMode = arguments.boolFlag("separate-parent-calls") ? MergeMode.separateParentCalls(methodDisambiguator) : MergeMode.sameMethod(methodDisambiguator);
future.thenAcceptAsync(s -> {
resp.broadcastPrefixed(TextComponent.of("The active sampling operation has completed! Uploading results..."));
- handleUpload(platform, resp, s, threadOrder);
+ handleUpload(platform, resp, s, threadOrder, mergeMode);
});
}
})
@@ -236,13 +241,12 @@ public class SamplerModule implements CommandModule {
}
if (arguments.contains("--stop") || arguments.contains("--upload")) {
- return TabCompleter.completeForOpts(arguments, "--order-by-time");
+ return TabCompleter.completeForOpts(arguments, "--order-by-time", "--separate-parent-calls");
}
List<String> opts = new ArrayList<>(Arrays.asList("--info", "--stop", "--cancel",
"--timeout", "--regex", "--combine-all", "--not-combined", "--interval",
- "--only-ticks-over", "--include-line-numbers", "--ignore-sleeping",
- "--order-by-time"));
+ "--only-ticks-over", "--ignore-sleeping", "--order-by-time", "--separate-parent-calls"));
opts.removeAll(arguments);
opts.add("--thread"); // allowed multiple times
@@ -254,9 +258,9 @@ public class SamplerModule implements CommandModule {
);
}
- private void handleUpload(SparkPlatform platform, CommandResponseHandler resp, Sampler sampler, ThreadNodeOrder threadOrder) {
+ private void handleUpload(SparkPlatform platform, CommandResponseHandler resp, Sampler sampler, ThreadNodeOrder threadOrder, MergeMode mergeMode) {
platform.getPlugin().executeAsync(() -> {
- byte[] output = sampler.formCompressedDataPayload(resp.sender(), threadOrder);
+ byte[] output = sampler.formCompressedDataPayload(resp.sender(), threadOrder, mergeMode);
try {
String key = SparkPlatform.BYTEBIN_CLIENT.postContent(output, SPARK_SAMPLER_MEDIA_TYPE, false).key();
String url = SparkPlatform.VIEWER_URL + key;
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 56093c0..6548d56 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
@@ -26,6 +26,7 @@ import me.lucko.spark.common.command.sender.CommandSender;
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;
@@ -80,16 +81,16 @@ public class Sampler implements Runnable {
/** 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 includeLineNumbers, boolean ignoreSleeping) {
+ public Sampler(int interval, ThreadDumper threadDumper, ThreadGrouper threadGrouper, long endTime, boolean ignoreSleeping) {
this.threadDumper = threadDumper;
- this.dataAggregator = new SimpleDataAggregator(this.workerPool, threadGrouper, interval, includeLineNumbers, ignoreSleeping);
+ this.dataAggregator = new SimpleDataAggregator(this.workerPool, threadGrouper, interval, ignoreSleeping);
this.interval = interval;
this.endTime = endTime;
}
- public Sampler(int interval, ThreadDumper threadDumper, ThreadGrouper threadGrouper, long endTime, boolean includeLineNumbers, boolean ignoreSleeping, TickHook tickHook, int tickLengthThreshold) {
+ public Sampler(int interval, ThreadDumper threadDumper, ThreadGrouper threadGrouper, long endTime, boolean ignoreSleeping, TickHook tickHook, int tickLengthThreshold) {
this.threadDumper = threadDumper;
- this.dataAggregator = new TickedDataAggregator(this.workerPool, threadGrouper, interval, includeLineNumbers, ignoreSleeping, tickHook, tickLengthThreshold);
+ this.dataAggregator = new TickedDataAggregator(this.workerPool, threadGrouper, interval, ignoreSleeping, tickHook, tickLengthThreshold);
this.interval = interval;
this.endTime = endTime;
}
@@ -160,7 +161,7 @@ public class Sampler implements Runnable {
}
}
- private SamplerData toProto(CommandSender creator, Comparator<? super Map.Entry<String, ThreadNode>> outputOrder) {
+ private SamplerData toProto(CommandSender creator, Comparator<? super Map.Entry<String, ThreadNode>> outputOrder, MergeMode mergeMode) {
SamplerData.Builder proto = SamplerData.newBuilder();
proto.setMetadata(SamplerMetadata.newBuilder()
.setUser(creator.toData().toProto())
@@ -175,16 +176,18 @@ public class Sampler implements Runnable {
data.sort(outputOrder);
for (Map.Entry<String, ThreadNode> entry : data) {
- proto.addThreads(entry.getValue().toProto());
+ proto.addThreads(entry.getValue().toProto(mergeMode));
}
return proto.build();
}
- public byte[] formCompressedDataPayload(CommandSender creator, Comparator<? super Map.Entry<String, ThreadNode>> outputOrder) {
+ public byte[] formCompressedDataPayload(CommandSender creator, Comparator<? super Map.Entry<String, ThreadNode>> outputOrder, MergeMode mergeMode) {
+ SamplerData proto = toProto(creator, outputOrder, mergeMode);
+
ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
try (OutputStream out = new GZIPOutputStream(byteOut)) {
- toProto(creator, outputOrder).writeTo(out);
+ proto.writeTo(out);
} catch (IOException e) {
throw new RuntimeException(e);
}
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 ae7fb89..ff4c6df 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
@@ -30,7 +30,6 @@ import java.util.concurrent.TimeUnit;
public class SamplerBuilder {
private double samplingInterval = 4; // milliseconds
- private boolean includeLineNumbers = false;
private boolean ignoreSleeping = false;
private long timeout = -1;
private ThreadDumper threadDumper = ThreadDumper.ALL;
@@ -71,11 +70,6 @@ public class SamplerBuilder {
return this;
}
- public SamplerBuilder includeLineNumbers(boolean includeLineNumbers) {
- this.includeLineNumbers = includeLineNumbers;
- return this;
- }
-
public SamplerBuilder ignoreSleeping(boolean ignoreSleeping) {
this.ignoreSleeping = ignoreSleeping;
return this;
@@ -86,9 +80,9 @@ public class SamplerBuilder {
int intervalMicros = (int) (this.samplingInterval * 1000d);
if (this.ticksOver == -1 || this.tickHook == null) {
- sampler = new Sampler(intervalMicros, this.threadDumper, this.threadGrouper, this.timeout, this.includeLineNumbers, this.ignoreSleeping);
+ sampler = new Sampler(intervalMicros, this.threadDumper, this.threadGrouper, this.timeout, this.ignoreSleeping);
} else {
- sampler = new Sampler(intervalMicros, this.threadDumper, this.threadGrouper, this.timeout, this.includeLineNumbers, this.ignoreSleeping, this.tickHook, this.ticksOver);
+ sampler = new Sampler(intervalMicros, this.threadDumper, this.threadGrouper, this.timeout, this.ignoreSleeping, this.tickHook, this.ticksOver);
}
sampler.start();
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 e72dfc5..8b401cc 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
@@ -42,17 +42,13 @@ public abstract class AbstractDataAggregator implements DataAggregator {
/** The interval to wait between sampling, in microseconds */
protected final int interval;
- /** If line numbers should be included in the output */
- private final boolean includeLineNumbers;
-
/** If sleeping threads should be ignored */
private final boolean ignoreSleeping;
- public AbstractDataAggregator(ExecutorService workerPool, ThreadGrouper threadGrouper, int interval, boolean includeLineNumbers, boolean ignoreSleeping) {
+ public AbstractDataAggregator(ExecutorService workerPool, ThreadGrouper threadGrouper, int interval, boolean ignoreSleeping) {
this.workerPool = workerPool;
this.threadGrouper = threadGrouper;
this.interval = interval;
- this.includeLineNumbers = includeLineNumbers;
this.ignoreSleeping = ignoreSleeping;
}
@@ -71,7 +67,7 @@ public abstract class AbstractDataAggregator implements DataAggregator {
try {
ThreadNode node = getNode(this.threadGrouper.getGroup(threadInfo.getThreadId(), threadInfo.getThreadName()));
- node.log(threadInfo.getStackTrace(), this.interval, this.includeLineNumbers);
+ node.log(threadInfo.getStackTrace(), this.interval);
} catch (Exception e) {
e.printStackTrace();
}
diff --git a/spark-common/src/main/java/me/lucko/spark/common/sampler/aggregator/SimpleDataAggregator.java b/spark-common/src/main/java/me/lucko/spark/common/sampler/aggregator/SimpleDataAggregator.java
index ef968fe..db74995 100644
--- a/spark-common/src/main/java/me/lucko/spark/common/sampler/aggregator/SimpleDataAggregator.java
+++ b/spark-common/src/main/java/me/lucko/spark/common/sampler/aggregator/SimpleDataAggregator.java
@@ -33,8 +33,8 @@ import java.util.concurrent.TimeUnit;
* Basic implementation of {@link DataAggregator}.
*/
public class SimpleDataAggregator extends AbstractDataAggregator {
- public SimpleDataAggregator(ExecutorService workerPool, ThreadGrouper threadGrouper, int interval, boolean includeLineNumbers, boolean ignoreSleeping) {
- super(workerPool, threadGrouper, interval, includeLineNumbers, ignoreSleeping);
+ public SimpleDataAggregator(ExecutorService workerPool, ThreadGrouper threadGrouper, int interval, boolean ignoreSleeping) {
+ super(workerPool, threadGrouper, interval, ignoreSleeping);
}
@Override
diff --git a/spark-common/src/main/java/me/lucko/spark/common/sampler/aggregator/TickedDataAggregator.java b/spark-common/src/main/java/me/lucko/spark/common/sampler/aggregator/TickedDataAggregator.java
index d8fda73..567c865 100644
--- a/spark-common/src/main/java/me/lucko/spark/common/sampler/aggregator/TickedDataAggregator.java
+++ b/spark-common/src/main/java/me/lucko/spark/common/sampler/aggregator/TickedDataAggregator.java
@@ -53,8 +53,8 @@ public class TickedDataAggregator extends AbstractDataAggregator {
private int currentTick = -1;
private TickList currentData = new TickList(0);
- public TickedDataAggregator(ExecutorService workerPool, ThreadGrouper threadGrouper, int interval, boolean includeLineNumbers, boolean ignoreSleeping, TickHook tickHook, int tickLengthThreshold) {
- super(workerPool, threadGrouper, interval, includeLineNumbers, ignoreSleeping);
+ public TickedDataAggregator(ExecutorService workerPool, ThreadGrouper threadGrouper, int interval, boolean ignoreSleeping, TickHook tickHook, int tickLengthThreshold) {
+ super(workerPool, threadGrouper, interval, ignoreSleeping);
this.tickHook = tickHook;
this.tickLengthThreshold = TimeUnit.MILLISECONDS.toMicros(tickLengthThreshold);
// 50 millis in a tick, plus 10 so we have a bit of room to go over
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 c5dde1c..fe2fd62 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
@@ -38,7 +38,7 @@ public abstract class AbstractNode {
/**
* A map of this nodes children
*/
- private final Map<String, StackTraceNode> children = new ConcurrentHashMap<>();
+ private final Map<StackTraceNode.Description, StackTraceNode> children = new ConcurrentHashMap<>();
/**
* The accumulated sample time for this node, measured in microseconds
@@ -54,20 +54,31 @@ public abstract class AbstractNode {
return this.totalTime.longValue() / 1000d;
}
- private AbstractNode resolveChild(String className, String methodName, int lineNumber) {
- String key = StackTraceNode.generateKey(className, methodName, lineNumber);
- StackTraceNode result = this.children.get(key); // fast path
+ /**
+ * Merge {@code other} into {@code this}.
+ *
+ * @param other the other node
+ */
+ public void merge(AbstractNode other) {
+ this.totalTime.add(other.totalTime.longValue());
+ for (Map.Entry<StackTraceNode.Description, StackTraceNode> child : other.children.entrySet()) {
+ resolveChild(child.getKey()).merge(child.getValue());
+ }
+ }
+
+ private AbstractNode resolveChild(StackTraceNode.Description description) {
+ StackTraceNode result = this.children.get(description); // fast path
if (result != null) {
return result;
}
- return this.children.computeIfAbsent(key, name -> new StackTraceNode(className, methodName, lineNumber));
+ return this.children.computeIfAbsent(description, name -> new StackTraceNode(description));
}
- public void log(StackTraceElement[] elements, long time, boolean includeLineNumbers) {
- log(elements, 0, time, includeLineNumbers);
+ public void log(StackTraceElement[] elements, long time) {
+ log(elements, 0, time);
}
- private void log(StackTraceElement[] elements, int offset, long time, boolean includeLineNumbers) {
+ private void log(StackTraceElement[] elements, int offset, long time) {
this.totalTime.add(time);
if (offset >= MAX_STACK_DEPTH) {
@@ -91,20 +102,35 @@ public abstract class AbstractNode {
StackTraceElement parent = offset == 0 ? null : elements[pointer + 1];
// get the line number of the parent element - the line which called "us"
- int lineNumber = parent == null || !includeLineNumbers ? StackTraceNode.NULL_LINE_NUMBER : parent.getLineNumber();
+ int parentLineNumber = parent == null ? StackTraceNode.NULL_LINE_NUMBER : parent.getLineNumber();
// resolve a child element within the structure for the element at pointer
- AbstractNode child = resolveChild(element.getClassName(), element.getMethodName(), lineNumber);
+ AbstractNode child = resolveChild(new StackTraceNode.Description(element.getClassName(), element.getMethodName(), element.getLineNumber(), parentLineNumber));
// call the log method on the found child, with an incremented offset.
- child.log(elements, offset + 1, time, includeLineNumbers);
+ child.log(elements, offset + 1, time);
}
- protected List<StackTraceNode> getChildren() {
+ protected List<StackTraceNode> exportChildren(MergeMode mergeMode) {
if (this.children.isEmpty()) {
return Collections.emptyList();
}
- List<StackTraceNode> list = new ArrayList<>(this.children.values());
+ List<StackTraceNode> list = new ArrayList<>(this.children.size());
+
+ outer:
+ for (StackTraceNode child : this.children.values()) {
+ // attempt to find an existing node we can merge into
+ for (StackTraceNode other : list) {
+ if (mergeMode.shouldMerge(other, child)) {
+ other.merge(child);
+ continue outer;
+ }
+ }
+
+ // just add
+ list.add(child);
+ }
+
list.sort(null);
return list;
}
diff --git a/spark-common/src/main/java/me/lucko/spark/common/sampler/node/MergeMode.java b/spark-common/src/main/java/me/lucko/spark/common/sampler/node/MergeMode.java
new file mode 100644
index 0000000..18a0ed3
--- /dev/null
+++ b/spark-common/src/main/java/me/lucko/spark/common/sampler/node/MergeMode.java
@@ -0,0 +1,90 @@
+/*
+ * This file is part of spark.
+ *
+ * Copyright (c) lucko (Luck) <luck@lucko.me>
+ * Copyright (c) contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package me.lucko.spark.common.sampler.node;
+
+import me.lucko.spark.common.util.MethodDisambiguator;
+
+import java.util.Objects;
+
+/**
+ * Function to determine if {@link StackTraceNode}s should be merged.
+ */
+public final class MergeMode {
+
+ public static MergeMode sameMethod(MethodDisambiguator methodDisambiguator) {
+ return new MergeMode(methodDisambiguator, false);
+ }
+
+ public static MergeMode separateParentCalls(MethodDisambiguator methodDisambiguator) {
+ return new MergeMode(methodDisambiguator, true);
+ }
+
+ private final MethodDisambiguator methodDisambiguator;
+ private final boolean separateParentCalls;
+
+ MergeMode(MethodDisambiguator methodDisambiguator, boolean separateParentCalls) {
+ this.methodDisambiguator = methodDisambiguator;
+ this.separateParentCalls = separateParentCalls;
+ }
+
+ public MethodDisambiguator getMethodDisambiguator() {
+ return this.methodDisambiguator;
+ }
+
+ public boolean separateParentCalls() {
+ return this.separateParentCalls;
+ }
+
+ /**
+ * Test if two stack trace nodes should be considered the same and merged.
+ *
+ * @param n1 the first node
+ * @param n2 the second node
+ * @return if the nodes should be merged
+ */
+ public boolean shouldMerge(StackTraceNode n1, StackTraceNode n2) {
+ // are the class names the same?
+ if (!n1.getClassName().equals(n2.getClassName())) {
+ return false;
+ }
+
+ // are the method names the same?
+ if (!n1.getMethodName().equals(n2.getMethodName())) {
+ return false;
+ }
+
+ // is the parent line the same? (did the same line of code call this method?)
+ if (this.separateParentCalls && n1.getParentLineNumber() != n2.getParentLineNumber()) {
+ return false;
+ }
+
+ // are the method descriptions the same? (is it the same method?)
+ String desc1 = this.methodDisambiguator.disambiguate(n1).map(MethodDisambiguator.MethodDescription::getDesc).orElse(null);
+ String desc2 = this.methodDisambiguator.disambiguate(n2).map(MethodDisambiguator.MethodDescription::getDesc).orElse(null);
+
+ if (desc1 == null && desc2 == null) {
+ return true;
+ }
+
+ return Objects.equals(desc1, desc2);
+ }
+
+}
diff --git a/spark-common/src/main/java/me/lucko/spark/common/sampler/node/StackTraceNode.java b/spark-common/src/main/java/me/lucko/spark/common/sampler/node/StackTraceNode.java
index 5b83c22..a73620e 100644
--- a/spark-common/src/main/java/me/lucko/spark/common/sampler/node/StackTraceNode.java
+++ b/spark-common/src/main/java/me/lucko/spark/common/sampler/node/StackTraceNode.java
@@ -21,8 +21,11 @@
package me.lucko.spark.common.sampler.node;
+import me.lucko.spark.common.util.MethodDisambiguator;
import me.lucko.spark.proto.SparkProtos;
+import java.util.Objects;
+
/**
* Represents a stack trace element within the {@link AbstractNode node} structure.
*/
@@ -33,60 +36,126 @@ public final class StackTraceNode extends AbstractNode implements Comparable<Sta
*/
public static final int NULL_LINE_NUMBER = -1;
- /**
- * Forms a key to represent the given node.
- *
- * @param className the name of the class
- * @param methodName the name of the method
- * @param lineNumber the line number of the parent method call
- * @return the key
- */
- static String generateKey(String className, String methodName, int lineNumber) {
- return className + "." + methodName + "." + lineNumber;
+ /** A description of the element */
+ private final Description description;
+
+ public StackTraceNode(Description description) {
+ this.description = description;
}
- /** The name of the class */
- private final String className;
- /** The name of the method */
- private final String methodName;
- /** The line number of the invocation which created this node */
- private final int lineNumber;
-
- public StackTraceNode(String className, String methodName, int lineNumber) {
- this.className = className;
- this.methodName = methodName;
- this.lineNumber = lineNumber;
+ public String getClassName() {
+ return this.description.className;
}
- public SparkProtos.StackTraceNode toProto() {
+ public String getMethodName() {
+ return this.description.methodName;
+ }
+
+ public int getLineNumber() {
+ return this.description.lineNumber;
+ }
+
+ public int getParentLineNumber() {
+ return this.description.parentLineNumber;
+ }
+
+ public SparkProtos.StackTraceNode toProto(MergeMode mergeMode) {
SparkProtos.StackTraceNode.Builder proto = SparkProtos.StackTraceNode.newBuilder()
.setTime(getTotalTime())
- .setClassName(this.className)
- .setMethodName(this.methodName);
+ .setClassName(this.description.className)
+ .setMethodName(this.description.methodName);
- if (this.lineNumber >= 0) {
- proto.setLineNumber(this.lineNumber);
+ if (this.description.lineNumber >= 0) {
+ proto.setLineNumber(this.description.lineNumber);
}
- for (StackTraceNode child : getChildren()) {
- proto.addChildren(child.toProto());
+ if (mergeMode.separateParentCalls() && this.description.parentLineNumber >= 0) {
+ proto.setParentLineNumber(this.description.parentLineNumber);
}
- return proto.build();
- }
+ mergeMode.getMethodDisambiguator().disambiguate(this)
+ .map(MethodDisambiguator.MethodDescription::getDesc)
+ .ifPresent(proto::setMethodDesc);
+
+ for (StackTraceNode child : exportChildren(mergeMode)) {
+ proto.addChildren(child.toProto(mergeMode));
+ }
- private String key() {
- return generateKey(this.className, this.methodName, this.lineNumber);
+ return proto.build();
}
@Override
public int compareTo(StackTraceNode that) {
+ if (this == that) {
+ return 0;
+ }
+
int i = -Double.compare(this.getTotalTime(), that.getTotalTime());
if (i != 0) {
return i;
}
- return this.key().compareTo(that.key());
+ return this.description.compareTo(that.description);
+ }
+
+ /**
+ * Encapsulates the attributes of a {@link StackTraceNode}.
+ */
+ public static final class Description implements Comparable<Description> {
+ private final String className;
+ private final String methodName;
+ private final int lineNumber;
+ private final int parentLineNumber;
+
+ private final int hash;
+
+ public Description(String className, String methodName, int lineNumber, int parentLineNumber) {
+ this.className = className;
+ this.methodName = methodName;
+ this.lineNumber = lineNumber;
+ this.parentLineNumber = parentLineNumber;
+ this.hash = Objects.hash(this.className, this.methodName, this.lineNumber, this.parentLineNumber);
+ }
+
+ @Override
+ public int compareTo(Description that) {
+ if (this == that) {
+ return 0;
+ }
+
+ int i = this.className.compareTo(that.className);
+ if (i != 0) {
+ return i;
+ }
+
+ i = this.methodName.compareTo(that.methodName);
+ if (i != 0) {
+ return i;
+ }
+
+ i = Integer.compare(this.lineNumber, that.lineNumber);
+ if (i != 0) {
+ return i;
+ }
+
+ return Integer.compare(this.parentLineNumber, that.parentLineNumber);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ Description description = (Description) o;
+ return this.lineNumber == description.lineNumber &&
+ this.parentLineNumber == description.parentLineNumber &&
+ this.className.equals(description.className) &&
+ this.methodName.equals(description.methodName);
+ }
+
+ @Override
+ public int hashCode() {
+ return this.hash;
+ }
}
}
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 471d2c1..5cac33d 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
@@ -36,13 +36,13 @@ public final class ThreadNode extends AbstractNode {
this.threadName = threadName;
}
- public SparkProtos.ThreadNode toProto() {
+ public SparkProtos.ThreadNode toProto(MergeMode mergeMode) {
SparkProtos.ThreadNode.Builder proto = SparkProtos.ThreadNode.newBuilder()
.setName(this.threadName)
.setTime(getTotalTime());
- for (StackTraceNode child : getChildren()) {
- proto.addChildren(child.toProto());
+ for (StackTraceNode child : exportChildren(mergeMode)) {
+ proto.addChildren(child.toProto(mergeMode));
}
return proto.build();
diff --git a/spark-common/src/main/java/me/lucko/spark/common/util/MethodDisambiguator.java b/spark-common/src/main/java/me/lucko/spark/common/util/MethodDisambiguator.java
new file mode 100644
index 0000000..3a150fd
--- /dev/null
+++ b/spark-common/src/main/java/me/lucko/spark/common/util/MethodDisambiguator.java
@@ -0,0 +1,156 @@
+/*
+ * This file is part of spark.
+ *
+ * Copyright (c) lucko (Luck) <luck@lucko.me>
+ * Copyright (c) contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package me.lucko.spark.common.util;
+
+import com.google.common.collect.ImmutableListMultimap;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ListMultimap;
+import me.lucko.spark.common.sampler.node.StackTraceNode;
+import org.objectweb.asm.ClassReader;
+import org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.Label;
+import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Opcodes;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Utility to disambiguate a method call (class + method name + line)
+ * to a method (method name + method description).
+ */
+public final class MethodDisambiguator {
+ private final Map<String, ComputedClass> cache = new ConcurrentHashMap<>();
+
+ public Optional<MethodDescription> disambiguate(StackTraceNode element) {
+ return disambiguate(element.getClassName(), element.getMethodName(), element.getLineNumber());
+ }
+
+ public Optional<MethodDescription> disambiguate(String className, String methodName, int lineNumber) {
+ ComputedClass computedClass = this.cache.get(className);
+ if (computedClass == null) {
+ try {
+ computedClass = compute(className);
+ } catch (Exception e) {
+ computedClass = ComputedClass.EMPTY;
+ }
+
+ // harmless race
+ this.cache.put(className, computedClass);
+ }
+
+ List<MethodDescription> descriptions = computedClass.descriptionsByName.get(methodName);
+ switch (descriptions.size()) {
+ case 0:
+ return Optional.empty();
+ case 1:
+ return Optional.of(descriptions.get(0));
+ default:
+ return Optional.ofNullable(computedClass.descriptionsByLine.get(lineNumber));
+ }
+ }
+
+ private static ClassReader getClassReader(String className) throws IOException {
+ String resource = className.replace('.', '/') + ".class";
+
+ try (InputStream is = ClassLoader.getSystemResourceAsStream(resource)) {
+ if (is != null) {
+ return new ClassReader(is);
+ }
+ }
+
+ try {
+ Class<?> clazz = Class.forName(className);
+ try (InputStream is = clazz.getClassLoader().getResourceAsStream(resource)) {
+ if (is != null) {
+ return new ClassReader(is);
+ }
+ }
+ } catch (ClassNotFoundException e) {
+ // ignore
+ }
+
+ throw new IOException("Unable to get resource: " + className);
+ }
+
+ private ComputedClass compute(String className) throws IOException {
+ ImmutableListMultimap.Builder<String, MethodDescription> descriptionsByName = ImmutableListMultimap.builder();
+ Map<Integer, MethodDescription> descriptionsByLine = new HashMap<>();
+
+ getClassReader(className).accept(new ClassVisitor(Opcodes.ASM7) {
+ @Override
+ public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
+ MethodDescription description = new MethodDescription(name, descriptor);
+ descriptionsByName.put(name, description);
+
+ return new MethodVisitor(Opcodes.ASM7) {
+ @Override
+ public void visitLineNumber(int line, Label start) {
+ descriptionsByLine.put(line, description);
+ }
+ };
+ }
+ }, Opcodes.ASM7);
+
+ return new ComputedClass(descriptionsByName.build(), ImmutableMap.copyOf(descriptionsByLine));
+ }
+
+ private static final class ComputedClass {
+ private static final ComputedClass EMPTY = new ComputedClass(ImmutableListMultimap.of(), ImmutableMap.of());
+
+ private final ListMultimap<String, MethodDescription> descriptionsByName;
+ private final Map<Integer, MethodDescription> descriptionsByLine;
+
+ private ComputedClass(ListMultimap<String, MethodDescription> descriptionsByName, Map<Integer, MethodDescription> descriptionsByLine) {
+ this.descriptionsByName = descriptionsByName;
+ this.descriptionsByLine = descriptionsByLine;
+ }
+ }
+
+ public static final class MethodDescription {
+ private final String name;
+ private final String desc;
+
+ private MethodDescription(String name, String desc) {
+ this.name = name;
+ this.desc = desc;
+ }
+
+ public String getName() {
+ return this.name;
+ }
+
+ public String getDesc() {
+ return this.desc;
+ }
+
+ @Override
+ public String toString() {
+ return this.name + this.desc;
+ }
+ }
+
+}