diff options
author | Luck <git@lucko.me> | 2020-03-06 17:27:26 +0000 |
---|---|---|
committer | Luck <git@lucko.me> | 2020-03-06 17:27:26 +0000 |
commit | 4eff32f040d023e225937c1a59583f1e89b2cac2 (patch) | |
tree | 5728fae71c32705cadea07c09acf868e2e4aafda /spark-common/src/main/java/me/lucko/spark | |
parent | 9b7dd3af75f0b2cc76dfd5a5d317874fa21b4856 (diff) | |
download | spark-4eff32f040d023e225937c1a59583f1e89b2cac2.tar.gz spark-4eff32f040d023e225937c1a59583f1e89b2cac2.tar.bz2 spark-4eff32f040d023e225937c1a59583f1e89b2cac2.zip |
Treat different methods (not just methods with the same name) as different stack nodes & include method descriptions in proto data
Diffstat (limited to 'spark-common/src/main/java/me/lucko/spark')
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; + } + } + +} |