diff --git a/build.gradle b/build.gradle index b95d2398..0e9389d3 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ allprojects { group = 'me.lucko' - version = '1.5-SNAPSHOT' + version = '1.6-SNAPSHOT' configurations { compileClasspath // Fabric-loom needs this for remap jar for some reason @@ -13,7 +13,7 @@ subprojects { apply plugin: 'idea' ext { - pluginVersion = '1.5.2' + pluginVersion = '1.6.0' pluginDescription = 'spark is a performance profiling plugin/mod for Minecraft clients, servers and proxies.' } diff --git a/spark-bukkit/src/main/java/me/lucko/spark/bukkit/BukkitClassSourceLookup.java b/spark-bukkit/src/main/java/me/lucko/spark/bukkit/BukkitClassSourceLookup.java new file mode 100644 index 00000000..a87be6f8 --- /dev/null +++ b/spark-bukkit/src/main/java/me/lucko/spark/bukkit/BukkitClassSourceLookup.java @@ -0,0 +1,52 @@ +/* + * This file is part of spark. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package me.lucko.spark.bukkit; + +import me.lucko.spark.common.util.ClassSourceLookup; + +import org.bukkit.plugin.java.JavaPlugin; + +import java.lang.reflect.Method; + +public class BukkitClassSourceLookup extends ClassSourceLookup.ByClassLoader { + private static final Class PLUGIN_CLASS_LOADER; + private static final Method GET_PLUGIN_METHOD; + + static { + try { + PLUGIN_CLASS_LOADER = Class.forName("org.bukkit.plugin.java.PluginClassLoader"); + GET_PLUGIN_METHOD = PLUGIN_CLASS_LOADER.getDeclaredMethod("getPlugin"); + GET_PLUGIN_METHOD.setAccessible(true); + } catch (ReflectiveOperationException e) { + throw new ExceptionInInitializerError(e); + } + } + + @Override + public String identify(ClassLoader loader) throws ReflectiveOperationException { + if (PLUGIN_CLASS_LOADER.isInstance(loader)) { + JavaPlugin plugin = (JavaPlugin) GET_PLUGIN_METHOD.invoke(loader); + return plugin.getName(); + } + return null; + } +} + diff --git a/spark-bukkit/src/main/java/me/lucko/spark/bukkit/BukkitSparkPlugin.java b/spark-bukkit/src/main/java/me/lucko/spark/bukkit/BukkitSparkPlugin.java index 970c4b48..e746d609 100644 --- a/spark-bukkit/src/main/java/me/lucko/spark/bukkit/BukkitSparkPlugin.java +++ b/spark-bukkit/src/main/java/me/lucko/spark/bukkit/BukkitSparkPlugin.java @@ -29,6 +29,7 @@ import me.lucko.spark.common.sampler.ThreadDumper; import me.lucko.spark.common.tick.TickHook; import me.lucko.spark.common.tick.TickReporter; +import me.lucko.spark.common.util.ClassSourceLookup; import net.kyori.adventure.platform.bukkit.BukkitAudiences; @@ -159,6 +160,11 @@ public TickReporter createTickReporter() { return null; } + @Override + public ClassSourceLookup createClassSourceLookup() { + return new BukkitClassSourceLookup(); + } + @Override public PlatformInfo getPlatformInfo() { return new BukkitPlatformInfo(getServer()); diff --git a/spark-bungeecord/src/main/java/me/lucko/spark/bungeecord/BungeeCordClassSourceLookup.java b/spark-bungeecord/src/main/java/me/lucko/spark/bungeecord/BungeeCordClassSourceLookup.java new file mode 100644 index 00000000..e601f876 --- /dev/null +++ b/spark-bungeecord/src/main/java/me/lucko/spark/bungeecord/BungeeCordClassSourceLookup.java @@ -0,0 +1,52 @@ +/* + * This file is part of spark. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package me.lucko.spark.bungeecord; + +import me.lucko.spark.common.util.ClassSourceLookup; + +import net.md_5.bungee.api.plugin.PluginDescription; + +import java.lang.reflect.Field; + +public class BungeeCordClassSourceLookup extends ClassSourceLookup.ByClassLoader { + private static final Class PLUGIN_CLASS_LOADER; + private static final Field DESC_FIELD; + + static { + try { + PLUGIN_CLASS_LOADER = Class.forName("net.md_5.bungee.api.plugin.PluginClassloader"); + DESC_FIELD = PLUGIN_CLASS_LOADER.getDeclaredField("desc"); + DESC_FIELD.setAccessible(true); + } catch (ReflectiveOperationException e) { + throw new ExceptionInInitializerError(e); + } + } + + @Override + public String identify(ClassLoader loader) throws ReflectiveOperationException { + if (PLUGIN_CLASS_LOADER.isInstance(loader)) { + PluginDescription desc = (PluginDescription) DESC_FIELD.get(loader); + return desc.getName(); + } + return null; + } +} + diff --git a/spark-bungeecord/src/main/java/me/lucko/spark/bungeecord/BungeeCordSparkPlugin.java b/spark-bungeecord/src/main/java/me/lucko/spark/bungeecord/BungeeCordSparkPlugin.java index 357c457a..2fb44c3f 100644 --- a/spark-bungeecord/src/main/java/me/lucko/spark/bungeecord/BungeeCordSparkPlugin.java +++ b/spark-bungeecord/src/main/java/me/lucko/spark/bungeecord/BungeeCordSparkPlugin.java @@ -23,6 +23,7 @@ import me.lucko.spark.common.SparkPlatform; import me.lucko.spark.common.SparkPlugin; import me.lucko.spark.common.platform.PlatformInfo; +import me.lucko.spark.common.util.ClassSourceLookup; import net.kyori.adventure.platform.bungeecord.BungeeAudiences; import net.md_5.bungee.api.CommandSender; @@ -78,6 +79,11 @@ public void executeAsync(Runnable task) { getProxy().getScheduler().runAsync(BungeeCordSparkPlugin.this, task); } + @Override + public ClassSourceLookup createClassSourceLookup() { + return new BungeeCordClassSourceLookup(); + } + @Override public PlatformInfo getPlatformInfo() { return new BungeeCordPlatformInfo(getProxy()); diff --git a/spark-common/build.gradle b/spark-common/build.gradle index e07647d7..59e6439a 100644 --- a/spark-common/build.gradle +++ b/spark-common/build.gradle @@ -9,6 +9,7 @@ dependencies { compile 'com.google.protobuf:protobuf-javalite:3.15.6' compile 'com.squareup.okhttp3:okhttp:3.14.1' compile 'com.squareup.okio:okio:1.17.3' + compile 'net.bytebuddy:byte-buddy-agent:1.11.0' compile 'org.tukaani:xz:1.8' compile('net.kyori:adventure-api:4.7.0') { exclude(module: 'checker-qual') 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 491b7d35..fdced75c 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 @@ -44,6 +44,7 @@ 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 net.kyori.adventure.text.event.ClickEvent; @@ -87,6 +88,7 @@ public class SparkPlatform { private final ActivityLog activityLog; private final TickHook tickHook; private final TickReporter tickReporter; + private final ClassSourceLookup classSourceLookup; private final TickStatistics tickStatistics; private Map startupGcStatistics = ImmutableMap.of(); private long serverNormalOperationStartTime; @@ -115,6 +117,7 @@ public SparkPlatform(SparkPlugin plugin) { this.tickHook = plugin.createTickHook(); this.tickReporter = plugin.createTickReporter(); + this.classSourceLookup = plugin.createClassSourceLookup(); this.tickStatistics = this.tickHook != null ? new TickStatistics() : null; } @@ -175,6 +178,10 @@ public TickReporter getTickReporter() { return this.tickReporter; } + public ClassSourceLookup getClassSourceLookup() { + return this.classSourceLookup; + } + public TickStatistics getTickStatistics() { return this.tickStatistics; } diff --git a/spark-common/src/main/java/me/lucko/spark/common/SparkPlugin.java b/spark-common/src/main/java/me/lucko/spark/common/SparkPlugin.java index 216f23f0..aa5112d6 100644 --- a/spark-common/src/main/java/me/lucko/spark/common/SparkPlugin.java +++ b/spark-common/src/main/java/me/lucko/spark/common/SparkPlugin.java @@ -26,6 +26,7 @@ import me.lucko.spark.common.sampler.ThreadDumper; import me.lucko.spark.common.tick.TickHook; import me.lucko.spark.common.tick.TickReporter; +import me.lucko.spark.common.util.ClassSourceLookup; import java.nio.file.Path; import java.util.stream.Stream; @@ -101,6 +102,15 @@ default TickReporter createTickReporter() { return null; } + /** + * Creates a class source lookup function. + * + * @return the class source lookup function + */ + default ClassSourceLookup createClassSourceLookup() { + return ClassSourceLookup.NO_OP; + } + /** * Gets information for the platform. * 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 856a1827..ebf63722 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 @@ -299,7 +299,7 @@ private void profilerStop(SparkPlatform platform, CommandSender sender, CommandR } private void handleUpload(SparkPlatform platform, CommandResponseHandler resp, Sampler sampler, ThreadNodeOrder threadOrder, String comment, MergeMode mergeMode) { - byte[] output = sampler.formCompressedDataPayload(platform.getPlugin().getPlatformInfo(), resp.sender(), threadOrder, comment, mergeMode); + byte[] output = sampler.formCompressedDataPayload(new Sampler.ExportProps(platform.getPlugin().getPlatformInfo(), resp.sender(), threadOrder, comment, mergeMode, platform.getClassSourceLookup())); try { String key = SparkPlatform.BYTEBIN_CLIENT.postContent(output, SPARK_SAMPLER_MEDIA_TYPE).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 5088ed78..bc08dfdb 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 @@ -24,6 +24,7 @@ import me.lucko.spark.common.platform.PlatformInfo; import me.lucko.spark.common.sampler.node.MergeMode; import me.lucko.spark.common.sampler.node.ThreadNode; +import me.lucko.spark.common.util.ClassSourceLookup; import me.lucko.spark.proto.SparkProtos.SamplerData; import java.io.ByteArrayOutputStream; @@ -71,16 +72,10 @@ public interface Sampler { CompletableFuture getFuture(); // Methods used to export the sampler data to the web viewer. - SamplerData toProto( - PlatformInfo platformInfo, - CommandSender creator, - Comparator> outputOrder, - String comment, - MergeMode mergeMode - ); + SamplerData toProto(ExportProps props); - default byte[] formCompressedDataPayload(PlatformInfo platformInfo, CommandSender creator, Comparator> outputOrder, String comment, MergeMode mergeMode) { - SamplerData proto = toProto(platformInfo, creator, outputOrder, comment, mergeMode); + default byte[] formCompressedDataPayload(ExportProps props) { + SamplerData proto = toProto(props); ByteArrayOutputStream byteOut = new ByteArrayOutputStream(); try (OutputStream out = new GZIPOutputStream(byteOut)) { @@ -91,4 +86,22 @@ default byte[] formCompressedDataPayload(PlatformInfo platformInfo, CommandSende return byteOut.toByteArray(); } + class ExportProps { + public final PlatformInfo platformInfo; + public final CommandSender creator; + public final Comparator> outputOrder; + public final String comment; + public final MergeMode mergeMode; + public final ClassSourceLookup classSourceLookup; + + public ExportProps(PlatformInfo platformInfo, CommandSender creator, Comparator> outputOrder, String comment, MergeMode mergeMode, ClassSourceLookup classSourceLookup) { + this.platformInfo = platformInfo; + this.creator = creator; + this.outputOrder = outputOrder; + this.comment = comment; + this.mergeMode = mergeMode; + this.classSourceLookup = classSourceLookup; + } + } + } 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 c76274b2..8d57a6d8 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 @@ -20,14 +20,12 @@ package me.lucko.spark.common.sampler.async; -import me.lucko.spark.common.command.sender.CommandSender; -import me.lucko.spark.common.platform.PlatformInfo; import me.lucko.spark.common.sampler.Sampler; 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.node.MergeMode; import me.lucko.spark.common.sampler.node.ThreadNode; +import me.lucko.spark.common.util.ClassSourceLookup; import me.lucko.spark.proto.SparkProtos; import one.profiler.AsyncProfiler; @@ -37,7 +35,6 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; -import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; @@ -144,17 +141,17 @@ public void stop() { } @Override - public SparkProtos.SamplerData toProto(PlatformInfo platformInfo, CommandSender creator, Comparator> outputOrder, String comment, MergeMode mergeMode) { + public SparkProtos.SamplerData toProto(ExportProps props) { final SparkProtos.SamplerMetadata.Builder metadata = SparkProtos.SamplerMetadata.newBuilder() - .setPlatformMetadata(platformInfo.toData().toProto()) - .setCreator(creator.toData().toProto()) + .setPlatformMetadata(props.platformInfo.toData().toProto()) + .setCreator(props.creator.toData().toProto()) .setStartTime(this.startTime) .setInterval(this.interval) .setThreadDumper(this.threadDumper.getMetadata()) .setDataAggregator(this.dataAggregator.getMetadata()); - if (comment != null) { - metadata.setComment(comment); + if (props.comment != null) { + metadata.setComment(props.comment); } SparkProtos.SamplerData.Builder proto = SparkProtos.SamplerData.newBuilder(); @@ -163,10 +160,17 @@ public SparkProtos.SamplerData toProto(PlatformInfo platformInfo, CommandSender aggregateOutput(); List> data = new ArrayList<>(this.dataAggregator.getData().entrySet()); - data.sort(outputOrder); + data.sort(props.outputOrder); + + ClassSourceLookup.Visitor classSourceVisitor = ClassSourceLookup.createVisitor(props.classSourceLookup); for (Map.Entry entry : data) { - proto.addThreads(entry.getValue().toProto(mergeMode)); + proto.addThreads(entry.getValue().toProto(props.mergeMode)); + classSourceVisitor.visit(entry.getValue()); + } + + if (classSourceVisitor.hasMappings()) { + proto.putAllClassSources(classSourceVisitor.getMapping()); } return proto.build(); diff --git a/spark-common/src/main/java/me/lucko/spark/common/sampler/java/JavaSampler.java b/spark-common/src/main/java/me/lucko/spark/common/sampler/java/JavaSampler.java index 5fe5add6..23d38d8d 100644 --- a/spark-common/src/main/java/me/lucko/spark/common/sampler/java/JavaSampler.java +++ b/spark-common/src/main/java/me/lucko/spark/common/sampler/java/JavaSampler.java @@ -23,14 +23,12 @@ 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.Sampler; import me.lucko.spark.common.sampler.ThreadDumper; import me.lucko.spark.common.sampler.ThreadGrouper; -import me.lucko.spark.common.sampler.node.MergeMode; import me.lucko.spark.common.sampler.node.ThreadNode; import me.lucko.spark.common.tick.TickHook; +import me.lucko.spark.common.util.ClassSourceLookup; import me.lucko.spark.proto.SparkProtos.SamplerData; import me.lucko.spark.proto.SparkProtos.SamplerMetadata; @@ -38,7 +36,6 @@ 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; @@ -161,27 +158,34 @@ public void run() { } @Override - public SamplerData toProto(PlatformInfo platformInfo, CommandSender creator, Comparator> outputOrder, String comment, MergeMode mergeMode) { + public SamplerData toProto(ExportProps props) { final SamplerMetadata.Builder metadata = SamplerMetadata.newBuilder() - .setPlatformMetadata(platformInfo.toData().toProto()) - .setCreator(creator.toData().toProto()) + .setPlatformMetadata(props.platformInfo.toData().toProto()) + .setCreator(props.creator.toData().toProto()) .setStartTime(this.startTime) .setInterval(this.interval) .setThreadDumper(this.threadDumper.getMetadata()) .setDataAggregator(this.dataAggregator.getMetadata()); - if (comment != null) { - metadata.setComment(comment); + if (props.comment != null) { + metadata.setComment(props.comment); } SamplerData.Builder proto = SamplerData.newBuilder(); proto.setMetadata(metadata.build()); List> data = new ArrayList<>(this.dataAggregator.getData().entrySet()); - data.sort(outputOrder); + data.sort(props.outputOrder); + + ClassSourceLookup.Visitor classSourceVisitor = ClassSourceLookup.createVisitor(props.classSourceLookup); for (Map.Entry entry : data) { - proto.addThreads(entry.getValue().toProto(mergeMode)); + proto.addThreads(entry.getValue().toProto(props.mergeMode)); + classSourceVisitor.visit(entry.getValue()); + } + + if (classSourceVisitor.hasMappings()) { + proto.putAllClassSources(classSourceVisitor.getMapping()); } return proto.build(); 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 2ef06d38..73f7bd70 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 @@ -24,6 +24,7 @@ import me.lucko.spark.common.sampler.async.AsyncStackTraceElement; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; @@ -56,6 +57,10 @@ public double getTotalTime() { return this.totalTime.longValue() / 1000d; } + public Collection getChildren() { + return this.children.values(); + } + /** * Merge {@code other} into {@code this}. * 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 41794640..efc7f81a 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 @@ -51,6 +51,10 @@ public String getMethodName() { return this.description.methodName; } + public String getMethodDescription() { + return this.description.methodDescription; + } + public int getLineNumber() { return this.description.lineNumber; } diff --git a/spark-common/src/main/java/me/lucko/spark/common/util/ClassFinder.java b/spark-common/src/main/java/me/lucko/spark/common/util/ClassFinder.java new file mode 100644 index 00000000..44817865 --- /dev/null +++ b/spark-common/src/main/java/me/lucko/spark/common/util/ClassFinder.java @@ -0,0 +1,71 @@ +/* + * This file is part of spark. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package me.lucko.spark.common.util; + +import net.bytebuddy.agent.ByteBuddyAgent; + +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.lang.instrument.Instrumentation; +import java.util.HashMap; +import java.util.Map; + +/** + * Uses {@link Instrumentation} to find a class reference for given class names. + * + *

This is necessary as we don't always have access to the classloader for a given class.

+ */ +public class ClassFinder { + + private final Map> classes = new HashMap<>(); + + public ClassFinder() { + Instrumentation instrumentation; + try { + instrumentation = ByteBuddyAgent.install(); + } catch (Exception e) { + return; + } + + // obtain and cache loaded classes + for (Class loadedClass : instrumentation.getAllLoadedClasses()) { + this.classes.put(loadedClass.getName(), loadedClass); + } + } + + public @Nullable Class findClass(String className) { + // try instrumentation + Class clazz = this.classes.get(className); + if (clazz != null) { + return clazz; + } + + // try Class.forName + try { + return Class.forName(className); + } catch (Throwable e) { + // ignore + } + + return null; + } + +} diff --git a/spark-common/src/main/java/me/lucko/spark/common/util/ClassSourceLookup.java b/spark-common/src/main/java/me/lucko/spark/common/util/ClassSourceLookup.java new file mode 100644 index 00000000..27e3ec61 --- /dev/null +++ b/spark-common/src/main/java/me/lucko/spark/common/util/ClassSourceLookup.java @@ -0,0 +1,223 @@ +/* + * This file is part of spark. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package me.lucko.spark.common.util; + +import me.lucko.spark.common.sampler.node.StackTraceNode; +import me.lucko.spark.common.sampler.node.ThreadNode; + +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.Paths; +import java.security.CodeSource; +import java.security.ProtectionDomain; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +/** + * A function which defines the source of given {@link Class}es. + */ +public interface ClassSourceLookup { + + /** + * Identify the given class. + * + * @param clazz the class + * @return the source of the class + */ + @Nullable String identify(Class clazz) throws Exception; + + /** + * A no-operation {@link ClassSourceLookup}. + */ + ClassSourceLookup NO_OP = new ClassSourceLookup() { + @Override + public @Nullable String identify(Class clazz) { + return null; + } + }; + + /** + * A {@link ClassSourceLookup} which identifies classes based on their {@link ClassLoader}. + */ + abstract class ByClassLoader implements ClassSourceLookup { + + public abstract @Nullable String identify(ClassLoader loader) throws Exception; + + @Override + public final @Nullable String identify(Class clazz) throws Exception { + ClassLoader loader = clazz.getClassLoader(); + while (loader != null) { + String source = identify(loader); + if (source != null) { + return source; + } + loader = loader.getParent(); + } + return null; + } + } + + /** + * A {@link ClassSourceLookup} which identifies classes based on the first URL in a {@link URLClassLoader}. + */ + class ByFirstUrlSource extends ByClassLoader { + @Override + public @Nullable String identify(ClassLoader loader) throws IOException, URISyntaxException { + if (loader instanceof URLClassLoader) { + URLClassLoader urlClassLoader = (URLClassLoader) loader; + URL[] urls = urlClassLoader.getURLs(); + if (urls.length == 0) { + return null; + } + return identifyUrl(urls[0]); + } + return null; + } + } + + /** + * A {@link ClassSourceLookup} which identifies classes based on their {@link ProtectionDomain#getCodeSource()}. + */ + class ByCodeSource implements ClassSourceLookup { + @Override + public @Nullable String identify(Class clazz) throws URISyntaxException { + ProtectionDomain protectionDomain = clazz.getProtectionDomain(); + if (protectionDomain == null) { + return null; + } + CodeSource codeSource = protectionDomain.getCodeSource(); + if (codeSource == null) { + return null; + } + + URL url = codeSource.getLocation(); + return url == null ? null : identifyUrl(url); + } + } + + /** + * Attempts to identify a jar file from a URL. + * + * @param url the url + * @return the name of the file + * @throws URISyntaxException thrown by {@link URL#toURI()} + */ + static String identifyUrl(URL url) throws URISyntaxException { + if (url.getProtocol().equals("file")) { + String jarName = Paths.get(url.toURI()).getFileName().toString(); + if (jarName.endsWith(".jar")) { + return jarName.substring(0, jarName.length() - 4); + } + } + return null; + } + interface Visitor { + void visit(ThreadNode node); + + boolean hasMappings(); + + Map getMapping(); + } + + static Visitor createVisitor(ClassSourceLookup lookup) { + if (lookup == ClassSourceLookup.NO_OP) { + return NoOpVistitor.INSTANCE; // don't bother! + } + return new VisitorImpl(lookup); + } + + enum NoOpVistitor implements Visitor { + INSTANCE; + + @Override + public void visit(ThreadNode node) { + + } + + @Override + public boolean hasMappings() { + return false; + } + + @Override + public Map getMapping() { + return Collections.emptyMap(); + } + } + + /** + * Visitor which scans {@link StackTraceNode}s and accumulates class identities. + */ + class VisitorImpl implements Visitor { + private final ClassSourceLookup lookup; + private final ClassFinder classFinder = new ClassFinder(); + + // class name --> identifier (plugin name) + private final Map map = new HashMap<>(); + + VisitorImpl(ClassSourceLookup lookup) { + this.lookup = lookup; + } + + @Override + public void visit(ThreadNode node) { + for (StackTraceNode child : node.getChildren()) { + visitStackNode(child); + } + } + + @Override + public boolean hasMappings() { + return !this.map.isEmpty(); + } + + @Override + public Map getMapping() { + this.map.values().removeIf(Objects::isNull); + return this.map; + } + + private void visitStackNode(StackTraceNode node) { + String className = node.getClassName(); + if (!this.map.containsKey(className)) { + try { + Class clazz = this.classFinder.findClass(className); + Objects.requireNonNull(clazz); + this.map.put(className, this.lookup.identify(clazz)); + } catch (Throwable e) { + this.map.put(className, null); + } + } + + // recursively + for (StackTraceNode child : node.getChildren()) { + visitStackNode(child); + } + } + } + +} 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 index 2e113a9c..a35bf083 100644 --- 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 @@ -48,6 +48,11 @@ public final class MethodDisambiguator { private final Map cache = new ConcurrentHashMap<>(); public Optional disambiguate(StackTraceNode element) { + String desc = element.getMethodDescription(); + if (desc != null) { + return Optional.of(new MethodDescription(element.getMethodName(), desc)); + } + return disambiguate(element.getClassName(), element.getMethodName(), element.getLineNumber()); } diff --git a/spark-common/src/main/proto/spark/spark.proto b/spark-common/src/main/proto/spark/spark.proto index 83d017a4..0734614b 100644 --- a/spark-common/src/main/proto/spark/spark.proto +++ b/spark-common/src/main/proto/spark/spark.proto @@ -45,7 +45,7 @@ message HeapData { message HeapMetadata { CommandSenderMetadata creator = 1; - PlatformMetadata platformMetadata = 2; + PlatformMetadata platform_metadata = 2; } message HeapEntry { @@ -58,6 +58,7 @@ message HeapEntry { message SamplerData { SamplerMetadata metadata = 1; repeated ThreadNode threads = 2; + map class_sources = 3; // optional } message SamplerMetadata { @@ -67,7 +68,7 @@ message SamplerMetadata { ThreadDumper thread_dumper = 4; DataAggregator data_aggregator = 5; string comment = 6; - PlatformMetadata platformMetadata = 7; + PlatformMetadata platform_metadata = 7; message ThreadDumper { Type type = 1; diff --git a/spark-fabric/build.gradle b/spark-fabric/build.gradle index eb978bdf..9fda9a83 100644 --- a/spark-fabric/build.gradle +++ b/spark-fabric/build.gradle @@ -61,6 +61,7 @@ shadowJar { relocate 'okhttp3', 'me.lucko.spark.lib.okhttp3' relocate 'net.kyori.adventure', 'me.lucko.spark.lib.adventure' relocate 'net.kyori.examination', 'me.lucko.spark.lib.adventure.examination' + relocate 'net.bytebuddy', 'me.lucko.spark.lib.bytebuddy' relocate 'org.tukaani.xz', 'me.lucko.spark.lib.xz' relocate 'com.google.protobuf', 'me.lucko.spark.lib.protobuf' relocate 'org.objectweb.asm', 'me.lucko.spark.lib.asm' diff --git a/spark-forge/build.gradle b/spark-forge/build.gradle index 29782d88..f74f135e 100644 --- a/spark-forge/build.gradle +++ b/spark-forge/build.gradle @@ -51,6 +51,7 @@ shadowJar { relocate 'okhttp3', 'me.lucko.spark.lib.okhttp3' relocate 'net.kyori.adventure', 'me.lucko.spark.lib.adventure' relocate 'net.kyori.examination', 'me.lucko.spark.lib.adventure.examination' + relocate 'net.bytebuddy', 'me.lucko.spark.lib.bytebuddy' relocate 'org.tukaani.xz', 'me.lucko.spark.lib.xz' relocate 'com.google.protobuf', 'me.lucko.spark.lib.protobuf' relocate 'org.objectweb.asm', 'me.lucko.spark.lib.asm' diff --git a/spark-forge1122/build.gradle b/spark-forge1122/build.gradle index 335b73ac..12c2d571 100644 --- a/spark-forge1122/build.gradle +++ b/spark-forge1122/build.gradle @@ -58,6 +58,7 @@ shadowJar { relocate 'okhttp3', 'me.lucko.spark.lib.okhttp3' relocate 'net.kyori.adventure', 'me.lucko.spark.lib.adventure' relocate 'net.kyori.examination', 'me.lucko.spark.lib.adventure.examination' + relocate 'net.bytebuddy', 'me.lucko.spark.lib.bytebuddy' relocate 'org.tukaani.xz', 'me.lucko.spark.lib.xz' relocate 'com.google.protobuf', 'me.lucko.spark.lib.protobuf' relocate 'org.objectweb.asm', 'me.lucko.spark.lib.asm' diff --git a/spark-nukkit/build.gradle b/spark-nukkit/build.gradle index 889fa0d3..0f2f04dc 100644 --- a/spark-nukkit/build.gradle +++ b/spark-nukkit/build.gradle @@ -28,6 +28,7 @@ shadowJar { relocate 'okio', 'me.lucko.spark.lib.okio' relocate 'okhttp3', 'me.lucko.spark.lib.okhttp3' relocate 'org.tukaani.xz', 'me.lucko.spark.lib.xz' + relocate 'net.bytebuddy', 'me.lucko.spark.lib.bytebuddy' relocate 'com.google.protobuf', 'me.lucko.spark.lib.protobuf' relocate 'org.objectweb.asm', 'me.lucko.spark.lib.asm' relocate 'one.profiler', 'me.lucko.spark.lib.asyncprofiler' diff --git a/spark-nukkit/src/main/java/me/lucko/spark/nukkit/NukkitClassSourceLookup.java b/spark-nukkit/src/main/java/me/lucko/spark/nukkit/NukkitClassSourceLookup.java new file mode 100644 index 00000000..9db4ee2d --- /dev/null +++ b/spark-nukkit/src/main/java/me/lucko/spark/nukkit/NukkitClassSourceLookup.java @@ -0,0 +1,41 @@ +/* + * This file is part of spark. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package me.lucko.spark.nukkit; + +import me.lucko.spark.common.util.ClassSourceLookup; + +import org.checkerframework.checker.nullness.qual.Nullable; + +import cn.nukkit.plugin.PluginClassLoader; + +import java.io.IOException; +import java.net.URISyntaxException; + +public class NukkitClassSourceLookup extends ClassSourceLookup.ByFirstUrlSource { + + @Override + public @Nullable String identify(ClassLoader loader) throws IOException, URISyntaxException { + if (loader instanceof PluginClassLoader) { + return super.identify(loader); + } + return null; + } +} diff --git a/spark-nukkit/src/main/java/me/lucko/spark/nukkit/NukkitSparkPlugin.java b/spark-nukkit/src/main/java/me/lucko/spark/nukkit/NukkitSparkPlugin.java index 96066144..b2db9f7e 100644 --- a/spark-nukkit/src/main/java/me/lucko/spark/nukkit/NukkitSparkPlugin.java +++ b/spark-nukkit/src/main/java/me/lucko/spark/nukkit/NukkitSparkPlugin.java @@ -24,6 +24,7 @@ import me.lucko.spark.common.SparkPlatform; import me.lucko.spark.common.SparkPlugin; import me.lucko.spark.common.platform.PlatformInfo; +import me.lucko.spark.common.util.ClassSourceLookup; import cn.nukkit.command.Command; import cn.nukkit.command.CommandSender; @@ -87,6 +88,11 @@ public void onRun() { }); } + @Override + public ClassSourceLookup createClassSourceLookup() { + return new NukkitClassSourceLookup(); + } + @Override public PlatformInfo getPlatformInfo() { return new NukkitPlatformInfo(getServer()); diff --git a/spark-sponge8/build.gradle b/spark-sponge8/build.gradle index 3f3a5904..a938bb2c 100644 --- a/spark-sponge8/build.gradle +++ b/spark-sponge8/build.gradle @@ -26,6 +26,7 @@ shadowJar { relocate 'okio', 'me.lucko.spark.lib.okio' relocate 'okhttp3', 'me.lucko.spark.lib.okhttp3' + relocate 'net.bytebuddy', 'me.lucko.spark.lib.bytebuddy' relocate 'org.tukaani.xz', 'me.lucko.spark.lib.xz' relocate 'com.google.protobuf', 'me.lucko.spark.lib.protobuf' relocate 'org.objectweb.asm', 'me.lucko.spark.lib.asm' diff --git a/spark-universal/build.gradle b/spark-universal/build.gradle index 8e4c780a..665dc657 100644 --- a/spark-universal/build.gradle +++ b/spark-universal/build.gradle @@ -16,6 +16,7 @@ shadowJar { relocate 'okhttp3', 'me.lucko.spark.lib.okhttp3' relocate 'net.kyori.adventure', 'me.lucko.spark.lib.adventure' relocate 'net.kyori.examination', 'me.lucko.spark.lib.adventure.examination' + relocate 'net.bytebuddy', 'me.lucko.spark.lib.bytebuddy' relocate 'org.tukaani.xz', 'me.lucko.spark.lib.xz' relocate 'com.google.protobuf', 'me.lucko.spark.lib.protobuf' relocate 'org.objectweb.asm', 'me.lucko.spark.lib.asm' diff --git a/spark-velocity/build.gradle b/spark-velocity/build.gradle index 28e2724e..8cce8580 100644 --- a/spark-velocity/build.gradle +++ b/spark-velocity/build.gradle @@ -24,6 +24,7 @@ shadowJar { relocate 'okio', 'me.lucko.spark.lib.okio' relocate 'okhttp3', 'me.lucko.spark.lib.okhttp3' + relocate 'net.bytebuddy', 'me.lucko.spark.lib.bytebuddy' relocate 'org.tukaani.xz', 'me.lucko.spark.lib.xz' relocate 'com.google.protobuf', 'me.lucko.spark.lib.protobuf' relocate 'org.objectweb.asm', 'me.lucko.spark.lib.asm' diff --git a/spark-velocity/src/main/java/me/lucko/spark/velocity/VelocityClassSourceLookup.java b/spark-velocity/src/main/java/me/lucko/spark/velocity/VelocityClassSourceLookup.java new file mode 100644 index 00000000..4989bf49 --- /dev/null +++ b/spark-velocity/src/main/java/me/lucko/spark/velocity/VelocityClassSourceLookup.java @@ -0,0 +1,59 @@ +/* + * This file is part of spark. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package me.lucko.spark.velocity; + +import com.velocitypowered.api.plugin.PluginContainer; +import com.velocitypowered.api.plugin.PluginManager; + +import me.lucko.spark.common.util.ClassSourceLookup; + +import org.checkerframework.checker.nullness.qual.Nullable; + +public class VelocityClassSourceLookup extends ClassSourceLookup.ByClassLoader { + private static final Class PLUGIN_CLASS_LOADER; + + static { + try { + PLUGIN_CLASS_LOADER = Class.forName("com.velocitypowered.proxy.plugin.PluginClassLoader"); + } catch (ReflectiveOperationException e) { + throw new ExceptionInInitializerError(e); + } + } + + private final PluginManager pluginManager; + + public VelocityClassSourceLookup(PluginManager pluginManager) { + this.pluginManager = pluginManager; + } + + @Override + public @Nullable String identify(ClassLoader loader) { + if (PLUGIN_CLASS_LOADER.isInstance(loader)) { + for (PluginContainer plugin : this.pluginManager.getPlugins()) { + Object instance = plugin.getInstance().orElse(null); + if (instance != null && instance.getClass().getClassLoader() == loader) { + return plugin.getDescription().getName().orElseGet(() -> plugin.getDescription().getId()); + } + } + } + return null; + } +} diff --git a/spark-velocity/src/main/java/me/lucko/spark/velocity/VelocitySparkPlugin.java b/spark-velocity/src/main/java/me/lucko/spark/velocity/VelocitySparkPlugin.java index ba32a3ea..40c470f3 100644 --- a/spark-velocity/src/main/java/me/lucko/spark/velocity/VelocitySparkPlugin.java +++ b/spark-velocity/src/main/java/me/lucko/spark/velocity/VelocitySparkPlugin.java @@ -33,6 +33,7 @@ import me.lucko.spark.common.SparkPlatform; import me.lucko.spark.common.SparkPlugin; import me.lucko.spark.common.platform.PlatformInfo; +import me.lucko.spark.common.util.ClassSourceLookup; import java.nio.file.Path; import java.util.List; @@ -108,6 +109,11 @@ public void executeAsync(Runnable task) { this.proxy.getScheduler().buildTask(this, task).schedule(); } + @Override + public ClassSourceLookup createClassSourceLookup() { + return new VelocityClassSourceLookup(this.proxy.getPluginManager()); + } + @Override public PlatformInfo getPlatformInfo() { return new VelocityPlatformInfo(this.proxy); diff --git a/spark-velocity2/build.gradle b/spark-velocity2/build.gradle index ebb925fe..184c2a34 100644 --- a/spark-velocity2/build.gradle +++ b/spark-velocity2/build.gradle @@ -27,6 +27,7 @@ shadowJar { relocate 'okio', 'me.lucko.spark.lib.okio' relocate 'okhttp3', 'me.lucko.spark.lib.okhttp3' + relocate 'net.bytebuddy', 'me.lucko.spark.lib.bytebuddy' relocate 'org.tukaani.xz', 'me.lucko.spark.lib.xz' relocate 'com.google.protobuf', 'me.lucko.spark.lib.protobuf' relocate 'org.objectweb.asm', 'me.lucko.spark.lib.asm' diff --git a/spark-velocity2/src/main/java/me/lucko/spark/velocity/Velocity2ClassSourceLookup.java b/spark-velocity2/src/main/java/me/lucko/spark/velocity/Velocity2ClassSourceLookup.java new file mode 100644 index 00000000..8374f097 --- /dev/null +++ b/spark-velocity2/src/main/java/me/lucko/spark/velocity/Velocity2ClassSourceLookup.java @@ -0,0 +1,59 @@ +/* + * This file is part of spark. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package me.lucko.spark.velocity; + +import com.velocitypowered.api.plugin.PluginContainer; +import com.velocitypowered.api.plugin.PluginManager; + +import me.lucko.spark.common.util.ClassSourceLookup; + +import org.checkerframework.checker.nullness.qual.Nullable; + +public class Velocity2ClassSourceLookup extends ClassSourceLookup.ByClassLoader { + private static final Class PLUGIN_CLASS_LOADER; + + static { + try { + PLUGIN_CLASS_LOADER = Class.forName("com.velocitypowered.proxy.plugin.PluginClassLoader"); + } catch (ReflectiveOperationException e) { + throw new ExceptionInInitializerError(e); + } + } + + private final PluginManager pluginManager; + + public Velocity2ClassSourceLookup(PluginManager pluginManager) { + this.pluginManager = pluginManager; + } + + @Override + public @Nullable String identify(ClassLoader loader) { + if (PLUGIN_CLASS_LOADER.isInstance(loader)) { + for (PluginContainer plugin : this.pluginManager.plugins()) { + Object instance = plugin.instance(); + if (instance != null && instance.getClass().getClassLoader() == loader) { + return plugin.description().name(); + } + } + } + return null; + } +} diff --git a/spark-velocity2/src/main/java/me/lucko/spark/velocity/Velocity2SparkPlugin.java b/spark-velocity2/src/main/java/me/lucko/spark/velocity/Velocity2SparkPlugin.java index a788a59e..0ea4bf8e 100644 --- a/spark-velocity2/src/main/java/me/lucko/spark/velocity/Velocity2SparkPlugin.java +++ b/spark-velocity2/src/main/java/me/lucko/spark/velocity/Velocity2SparkPlugin.java @@ -109,6 +109,11 @@ public void executeAsync(Runnable task) { this.proxy.scheduler().buildTask(this, task).schedule(); } + @Override + public ClassSourceLookup createClassSourceLookup() { + return new Velocity2ClassSourceLookup(this.proxy.pluginManager()); + } + @Override public PlatformInfo getPlatformInfo() { return new Velocity2PlatformInfo(this.proxy);