aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorishland <ishlandmc@yeah.net>2022-09-19 21:48:28 +0800
committerGitHub <noreply@github.com>2022-09-19 14:48:28 +0100
commit7ef9b6281135ce0a24f3c14c2255d9a2c2eca969 (patch)
tree59196f2253eb9ab3448339a64d68a426b48ce25f
parent618230b958d7822985e2702cd9528f1b4567e59c (diff)
downloadspark-7ef9b6281135ce0a24f3c14c2255d9a2c2eca969.tar.gz
spark-7ef9b6281135ce0a24f3c14c2255d9a2c2eca969.tar.bz2
spark-7ef9b6281135ce0a24f3c14c2255d9a2c2eca969.zip
Display source info for mixin injected methods (#249)
Co-authored-by: Luck <git@lucko.me>
-rw-r--r--spark-common/src/main/java/me/lucko/spark/common/sampler/AbstractSampler.java12
-rw-r--r--spark-common/src/main/java/me/lucko/spark/common/util/ClassSourceLookup.java257
-rw-r--r--spark-common/src/main/proto/spark/spark_sampler.proto2
-rw-r--r--spark-fabric/build.gradle10
-rw-r--r--spark-fabric/src/main/java/me/lucko/spark/fabric/FabricClassSourceLookup.java159
-rw-r--r--spark-fabric/src/main/java/me/lucko/spark/fabric/plugin/FabricSparkMixinPlugin.java71
-rw-r--r--spark-fabric/src/main/java/me/lucko/spark/fabric/smap/MixinUtils.java52
-rw-r--r--spark-fabric/src/main/java/me/lucko/spark/fabric/smap/SourceDebugCache.java87
-rw-r--r--spark-fabric/src/main/java/me/lucko/spark/fabric/smap/SourceMap.java133
-rw-r--r--spark-fabric/src/main/java/me/lucko/spark/fabric/smap/SourceMapProvider.java53
-rw-r--r--spark-fabric/src/main/resources/spark.mixins.json3
11 files changed, 806 insertions, 33 deletions
diff --git a/spark-common/src/main/java/me/lucko/spark/common/sampler/AbstractSampler.java b/spark-common/src/main/java/me/lucko/spark/common/sampler/AbstractSampler.java
index 1c217db..3cfef0b 100644
--- a/spark-common/src/main/java/me/lucko/spark/common/sampler/AbstractSampler.java
+++ b/spark-common/src/main/java/me/lucko/spark/common/sampler/AbstractSampler.java
@@ -164,8 +164,16 @@ public abstract class AbstractSampler implements Sampler {
classSourceVisitor.visit(entry);
}
- if (classSourceVisitor.hasMappings()) {
- proto.putAllClassSources(classSourceVisitor.getMapping());
+ if (classSourceVisitor.hasClassSourceMappings()) {
+ proto.putAllClassSources(classSourceVisitor.getClassSourceMapping());
+ }
+
+ if (classSourceVisitor.hasMethodSourceMappings()) {
+ proto.putAllMethodSources(classSourceVisitor.getMethodSourceMapping());
+ }
+
+ if (classSourceVisitor.hasLineSourceMappings()) {
+ proto.putAllLineSources(classSourceVisitor.getLineSourceMapping());
}
}
}
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
index bd9ec37..668f31a 100644
--- 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
@@ -38,9 +38,11 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
+import java.util.function.Function;
+import java.util.stream.Collectors;
/**
- * A function which defines the source of given {@link Class}es.
+ * A function which defines the source of given {@link Class}es or (Mixin) method calls.
*/
public interface ClassSourceLookup {
@@ -53,6 +55,26 @@ public interface ClassSourceLookup {
@Nullable String identify(Class<?> clazz) throws Exception;
/**
+ * Identify the given method call.
+ *
+ * @param methodCall the method call info
+ * @return the source of the method call
+ */
+ default @Nullable String identify(MethodCall methodCall) throws Exception {
+ return null;
+ }
+
+ /**
+ * Identify the given method call.
+ *
+ * @param methodCall the method call info
+ * @return the source of the method call
+ */
+ default @Nullable String identify(MethodCallByLine methodCall) throws Exception {
+ return null;
+ }
+
+ /**
* A no-operation {@link ClassSourceLookup}.
*/
ClassSourceLookup NO_OP = new ClassSourceLookup() {
@@ -156,9 +178,17 @@ public interface ClassSourceLookup {
interface Visitor {
void visit(ThreadNode node);
- boolean hasMappings();
+ boolean hasClassSourceMappings();
+
+ Map<String, String> getClassSourceMapping();
+
+ boolean hasMethodSourceMappings();
+
+ Map<String, String> getMethodSourceMapping();
+
+ boolean hasLineSourceMappings();
- Map<String, String> getMapping();
+ Map<String, String> getLineSourceMapping();
}
static Visitor createVisitor(ClassSourceLookup lookup) {
@@ -177,25 +207,46 @@ public interface ClassSourceLookup {
}
@Override
- public boolean hasMappings() {
+ public boolean hasClassSourceMappings() {
+ return false;
+ }
+
+ @Override
+ public Map<String, String> getClassSourceMapping() {
+ return Collections.emptyMap();
+ }
+
+ @Override
+ public boolean hasMethodSourceMappings() {
+ return false;
+ }
+
+ @Override
+ public Map<String, String> getMethodSourceMapping() {
+ return Collections.emptyMap();
+ }
+
+ @Override
+ public boolean hasLineSourceMappings() {
return false;
}
@Override
- public Map<String, String> getMapping() {
+ public Map<String, String> getLineSourceMapping() {
return Collections.emptyMap();
}
}
/**
- * Visitor which scans {@link StackTraceNode}s and accumulates class identities.
+ * Visitor which scans {@link StackTraceNode}s and accumulates class/method call identities.
*/
class VisitorImpl implements Visitor {
private final ClassSourceLookup lookup;
private final ClassFinder classFinder = new ClassFinder();
- // class name --> identifier (plugin name)
- private final Map<String, String> map = new HashMap<>();
+ private final SourcesMap<String> classSources = new SourcesMap<>(Function.identity());
+ private final SourcesMap<MethodCall> methodSources = new SourcesMap<>(MethodCall::toString);
+ private final SourcesMap<MethodCallByLine> lineSources = new SourcesMap<>(MethodCallByLine::toString);
VisitorImpl(ClassSourceLookup lookup) {
this.lookup = lookup;
@@ -208,34 +259,194 @@ public interface ClassSourceLookup {
}
}
+ private void visitStackNode(StackTraceNode node) {
+ this.classSources.computeIfAbsent(
+ node.getClassName(),
+ className -> {
+ Class<?> clazz = this.classFinder.findClass(className);
+ if (clazz == null) {
+ return null;
+ }
+ return this.lookup.identify(clazz);
+ });
+
+ if (node.getMethodDescription() != null) {
+ MethodCall methodCall = new MethodCall(node.getClassName(), node.getMethodName(), node.getMethodDescription());
+ this.methodSources.computeIfAbsent(methodCall, this.lookup::identify);
+ } else {
+ MethodCallByLine methodCall = new MethodCallByLine(node.getClassName(), node.getMethodName(), node.getLineNumber());
+ this.lineSources.computeIfAbsent(methodCall, this.lookup::identify);
+ }
+
+ // recursively
+ for (StackTraceNode child : node.getChildren()) {
+ visitStackNode(child);
+ }
+ }
+
@Override
- public boolean hasMappings() {
- return !this.map.isEmpty();
+ public boolean hasClassSourceMappings() {
+ return this.classSources.hasMappings();
}
@Override
- public Map<String, String> getMapping() {
- this.map.values().removeIf(Objects::isNull);
- return this.map;
+ public Map<String, String> getClassSourceMapping() {
+ return this.classSources.export();
}
- private void visitStackNode(StackTraceNode node) {
- String className = node.getClassName();
- if (!this.map.containsKey(className)) {
+ @Override
+ public boolean hasMethodSourceMappings() {
+ return this.methodSources.hasMappings();
+ }
+
+ @Override
+ public Map<String, String> getMethodSourceMapping() {
+ return this.methodSources.export();
+ }
+
+ @Override
+ public boolean hasLineSourceMappings() {
+ return this.lineSources.hasMappings();
+ }
+
+ @Override
+ public Map<String, String> getLineSourceMapping() {
+ return this.lineSources.export();
+ }
+ }
+
+ final class SourcesMap<T> {
+ // <key> --> identifier (plugin name)
+ private final Map<T, String> map = new HashMap<>();
+ private final Function<? super T, String> keyToStringFunction;
+
+ private SourcesMap(Function<? super T, String> keyToStringFunction) {
+ this.keyToStringFunction = keyToStringFunction;
+ }
+
+ public void computeIfAbsent(T key, ComputeSourceFunction<T> function) {
+ if (!this.map.containsKey(key)) {
try {
- Class<?> clazz = this.classFinder.findClass(className);
- Objects.requireNonNull(clazz);
- this.map.put(className, this.lookup.identify(clazz));
+ this.map.put(key, function.compute(key));
} catch (Throwable e) {
- this.map.put(className, null);
+ this.map.put(key, null);
}
}
+ }
- // recursively
- for (StackTraceNode child : node.getChildren()) {
- visitStackNode(child);
+ public boolean hasMappings() {
+ this.map.values().removeIf(Objects::isNull);
+ return !this.map.isEmpty();
+ }
+
+ public Map<String, String> export() {
+ this.map.values().removeIf(Objects::isNull);
+ if (this.keyToStringFunction.equals(Function.identity())) {
+ //noinspection unchecked
+ return (Map<String, String>) this.map;
+ } else {
+ return this.map.entrySet().stream().collect(Collectors.toMap(
+ e -> this.keyToStringFunction.apply(e.getKey()),
+ Map.Entry::getValue
+ ));
}
}
+
+ private interface ComputeSourceFunction<T> {
+ String compute(T key) throws Exception;
+ }
+ }
+
+ /**
+ * Encapsulates information about a given method call using the name + method description.
+ */
+ final class MethodCall {
+ private final String className;
+ private final String methodName;
+ private final String methodDescriptor;
+
+ public MethodCall(String className, String methodName, String methodDescriptor) {
+ this.className = className;
+ this.methodName = methodName;
+ this.methodDescriptor = methodDescriptor;
+ }
+
+ public String getClassName() {
+ return this.className;
+ }
+
+ public String getMethodName() {
+ return this.methodName;
+ }
+
+ public String getMethodDescriptor() {
+ return this.methodDescriptor;
+ }
+
+ @Override
+ public String toString() {
+ return this.className + ";" + this.methodName + ";" + this.methodDescriptor;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof MethodCall)) return false;
+ MethodCall that = (MethodCall) o;
+ return this.className.equals(that.className) &&
+ this.methodName.equals(that.methodName) &&
+ this.methodDescriptor.equals(that.methodDescriptor);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(this.className, this.methodName, this.methodDescriptor);
+ }
+ }
+
+ /**
+ * Encapsulates information about a given method call using the name + line number.
+ */
+ final class MethodCallByLine {
+ private final String className;
+ private final String methodName;
+ private final int lineNumber;
+
+ public MethodCallByLine(String className, String methodName, int lineNumber) {
+ this.className = className;
+ this.methodName = methodName;
+ this.lineNumber = lineNumber;
+ }
+
+ public String getClassName() {
+ return this.className;
+ }
+
+ public String getMethodName() {
+ return this.methodName;
+ }
+
+ public int getLineNumber() {
+ return this.lineNumber;
+ }
+
+ @Override
+ public String toString() {
+ return this.className + ";" + this.lineNumber;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof MethodCallByLine)) return false;
+ MethodCallByLine that = (MethodCallByLine) o;
+ return this.lineNumber == that.lineNumber && this.className.equals(that.className);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(this.className, this.lineNumber);
+ }
}
}
diff --git a/spark-common/src/main/proto/spark/spark_sampler.proto b/spark-common/src/main/proto/spark/spark_sampler.proto
index 8d9512a..f670ddf 100644
--- a/spark-common/src/main/proto/spark/spark_sampler.proto
+++ b/spark-common/src/main/proto/spark/spark_sampler.proto
@@ -11,6 +11,8 @@ message SamplerData {
SamplerMetadata metadata = 1;
repeated ThreadNode threads = 2;
map<string, string> class_sources = 3; // optional
+ map<string, string> method_sources = 4; // optional
+ map<string, string> line_sources = 5; // optional
}
message SamplerMetadata {
diff --git a/spark-fabric/build.gradle b/spark-fabric/build.gradle
index 30b1ff6..fce859a 100644
--- a/spark-fabric/build.gradle
+++ b/spark-fabric/build.gradle
@@ -66,6 +66,10 @@ processResources {
}
}
+license {
+ exclude '**/smap/SourceMap.java'
+}
+
shadowJar {
archiveFileName = "spark-fabric-${project.pluginVersion}-dev.jar"
configurations = [project.configurations.shade]
@@ -74,12 +78,16 @@ shadowJar {
relocate 'net.kyori.examination', 'me.lucko.spark.lib.adventure.examination'
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 'org.objectweb.asm', 'me.lucko.spark.lib.asm'
relocate 'one.profiler', 'me.lucko.spark.lib.asyncprofiler'
exclude 'module-info.class'
exclude 'META-INF/maven/**'
exclude 'META-INF/proguard/**'
+
+ dependencies {
+ exclude(dependency('org.ow2.asm::'))
+ }
}
task remappedShadowJar(type: RemapJarTask) {
diff --git a/spark-fabric/src/main/java/me/lucko/spark/fabric/FabricClassSourceLookup.java b/spark-fabric/src/main/java/me/lucko/spark/fabric/FabricClassSourceLookup.java
index 7030680..9ffac18 100644
--- a/spark-fabric/src/main/java/me/lucko/spark/fabric/FabricClassSourceLookup.java
+++ b/spark-fabric/src/main/java/me/lucko/spark/fabric/FabricClassSourceLookup.java
@@ -22,18 +22,35 @@ package me.lucko.spark.fabric;
import com.google.common.collect.ImmutableMap;
+import me.lucko.spark.common.util.ClassFinder;
import me.lucko.spark.common.util.ClassSourceLookup;
+import me.lucko.spark.fabric.smap.MixinUtils;
+import me.lucko.spark.fabric.smap.SourceMap;
+import me.lucko.spark.fabric.smap.SourceMapProvider;
import net.fabricmc.loader.api.FabricLoader;
import net.fabricmc.loader.api.ModContainer;
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.objectweb.asm.Type;
+import org.spongepowered.asm.mixin.FabricUtil;
+import org.spongepowered.asm.mixin.extensibility.IMixinConfig;
+import org.spongepowered.asm.mixin.transformer.Config;
+import org.spongepowered.asm.mixin.transformer.meta.MixinMerged;
+
+import java.lang.reflect.Method;
+import java.net.URI;
import java.nio.file.Path;
import java.util.Collection;
import java.util.Map;
public class FabricClassSourceLookup extends ClassSourceLookup.ByCodeSource {
+
+ private final ClassFinder classFinder = new ClassFinder();
+ private final SourceMapProvider smapProvider = new SourceMapProvider();
+
private final Path modsDirectory;
- private final Map<Path, String> pathToModMap;
+ private final Map<String, String> pathToModMap;
public FabricClassSourceLookup() {
FabricLoader loader = FabricLoader.getInstance();
@@ -43,7 +60,7 @@ public class FabricClassSourceLookup extends ClassSourceLookup.ByCodeSource {
@Override
public String identifyFile(Path path) {
- String id = this.pathToModMap.get(path);
+ String id = this.pathToModMap.get(path.toAbsolutePath().normalize().toString());
if (id != null) {
return id;
}
@@ -55,11 +72,141 @@ public class FabricClassSourceLookup extends ClassSourceLookup.ByCodeSource {
return super.identifyFileName(this.modsDirectory.relativize(path).toString());
}
- private static Map<Path, String> constructPathToModIdMap(Collection<ModContainer> mods) {
- ImmutableMap.Builder<Path, String> builder = ImmutableMap.builder();
+ @Override
+ public @Nullable String identify(MethodCall methodCall) throws Exception {
+ String className = methodCall.getClassName();
+ String methodName = methodCall.getMethodName();
+ String methodDesc = methodCall.getMethodDescriptor();
+
+ if (className.equals("native") || methodName.equals("<init>") || methodName.equals("<clinit>")) {
+ return null;
+ }
+
+ Class<?> clazz = this.classFinder.findClass(className);
+ if (clazz == null) {
+ return null;
+ }
+
+ Class<?>[] params = getParameterTypesForMethodDesc(methodDesc);
+ Method reflectMethod = clazz.getDeclaredMethod(methodName, params);
+
+ MixinMerged mixinMarker = reflectMethod.getDeclaredAnnotation(MixinMerged.class);
+ if (mixinMarker == null) {
+ return null;
+ }
+
+ return modIdFromMixinClass(mixinMarker.mixin());
+ }
+
+ @Override
+ public @Nullable String identify(MethodCallByLine methodCall) throws Exception {
+ String className = methodCall.getClassName();
+ String methodName = methodCall.getMethodName();
+ int lineNumber = methodCall.getLineNumber();
+
+ if (className.equals("native") || methodName.equals("<init>") || methodName.equals("<clinit>")) {
+ return null;
+ }
+
+ SourceMap smap = this.smapProvider.getSourceMap(className);
+ if (smap == null) {
+ return null;
+ }
+
+ int[] inputLineInfo = smap.getReverseLineMapping().get(lineNumber);
+ if (inputLineInfo == null || inputLineInfo.length == 0) {
+ return null;
+ }
+
+ for (int fileInfoIds : inputLineInfo) {
+ SourceMap.FileInfo inputFileInfo = smap.getFileInfo().get(fileInfoIds);
+ if (inputFileInfo == null) {
+ continue;
+ }
+
+ String path = inputFileInfo.path();
+ if (path.endsWith(".java")) {
+ path = path.substring(0, path.length() - 5);
+ }
+
+ String possibleMixinClassName = path.replace('/', '.');
+ if (possibleMixinClassName.equals(className)) {
+ continue;
+ }
+
+ return modIdFromMixinClass(possibleMixinClassName);
+ }
+
+ return null;
+ }
+
+ private static String modIdFromMixinClass(String mixinClassName) {
+ for (Config config : MixinUtils.getMixinConfigs().values()) {
+ IMixinConfig mixinConfig = config.getConfig();
+ if (mixinClassName.startsWith(mixinConfig.getMixinPackage())) {
+ return mixinConfig.getDecoration(FabricUtil.KEY_MOD_ID);
+ }
+ }
+ return null;
+ }
+
+ private Class<?>[] getParameterTypesForMethodDesc(String methodDesc) {
+ Type methodType = Type.getMethodType(methodDesc);
+ Class<?>[] params = new Class[methodType.getArgumentTypes().length];
+ Type[] argumentTypes = methodType.getArgumentTypes();
+
+ for (int i = 0, argumentTypesLength = argumentTypes.length; i < argumentTypesLength; i++) {
+ Type argumentType = argumentTypes[i];
+ params[i] = getClassFromType(argumentType);
+ }
+
+ return params;
+ }
+
+ private Class<?> getClassFromType(Type type) {
+ return switch (type.getSort()) {
+ case Type.VOID -> void.class;
+ case Type.BOOLEAN -> boolean.class;
+ case Type.CHAR -> char.class;
+ case Type.BYTE -> byte.class;
+ case Type.SHORT -> short.class;
+ case Type.INT -> int.class;
+ case Type.FLOAT -> float.class;
+ case Type.LONG -> long.class;
+ case Type.DOUBLE -> double.class;
+ case Type.ARRAY -> {
+ final Class<?> classFromType = getClassFromType(type.getElementType());
+ Class<?> result = classFromType;
+ if (classFromType != null) {
+ for (int i = 0; i < type.getDimensions(); i++) {
+ result = result.arrayType();
+ }
+ }
+ yield result;
+ }
+ case Type.OBJECT -> this.classFinder.findClass(type.getClassName());
+ default -> null;
+ };
+ }
+
+ private static Map<String, String> constructPathToModIdMap(Collection<ModContainer> mods) {
+ ImmutableMap.Builder<String, String> builder = ImmutableMap.builder();
for (ModContainer mod : mods) {
- Path path = mod.getRootPath().toAbsolutePath().normalize();
- builder.put(path, mod.getMetadata().getId());
+ String modId = mod.getMetadata().getId();
+ if (modId.equals("java")) {
+ continue;
+ }
+
+ for (Path path : mod.getRootPaths()) {
+ URI uri = path.toUri();
+ if (uri.getScheme().equals("jar") && path.toString().equals("/")) { // ZipFileSystem
+ String zipFilePath = path.getFileSystem().toString();
+ builder.put(zipFilePath, modId);
+ } else {
+ builder.put(path.toAbsolutePath().normalize().toString(), modId);
+ }
+
+ }
}
return builder.build();
}
diff --git a/spark-fabric/src/main/java/me/lucko/spark/fabric/plugin/FabricSparkMixinPlugin.java b/spark-fabric/src/main/java/me/lucko/spark/fabric/plugin/FabricSparkMixinPlugin.java
new file mode 100644
index 0000000..cfc8c95
--- /dev/null
+++ b/spark-fabric/src/main/java/me/lucko/spark/fabric/plugin/FabricSparkMixinPlugin.java
@@ -0,0 +1,71 @@
+/*
+ * 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.fabric.plugin;
+
+import me.lucko.spark.fabric.smap.SourceDebugCache;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.objectweb.asm.tree.ClassNode;
+import org.spongepowered.asm.mixin.MixinEnvironment;
+import org.spongepowered.asm.mixin.extensibility.IMixinConfigPlugin;
+import org.spongepowered.asm.mixin.extensibility.IMixinInfo;
+import org.spongepowered.asm.mixin.transformer.IMixinTransformer;
+import org.spongepowered.asm.mixin.transformer.ext.Extensions;
+import org.spongepowered.asm.mixin.transformer.ext.IExtension;
+import org.spongepowered.asm.mixin.transformer.ext.ITargetClassContext;
+
+import java.util.List;
+import java.util.Set;
+
+public class FabricSparkMixinPlugin implements IMixinConfigPlugin, IExtension {
+
+ private static final Logger LOGGER = LogManager.getLogger("spark");
+
+ @Override
+ public void onLoad(String mixinPackage) {
+ Object activeTransformer = MixinEnvironment.getCurrentEnvironment().getActiveTransformer();
+ if (activeTransformer instanceof IMixinTransformer transformer && transformer.getExtensions() instanceof Extensions extensions) {
+ extensions.add(this);
+ } else {
+ LOGGER.error(
+ "Failed to initialize SMAP parser for spark profiler. " +
+ "Mod information for mixin injected methods is now only available with the async-profiler engine."
+ );
+ }
+ }
+
+ @Override
+ public void export(MixinEnvironment env, String name, boolean force, ClassNode classNode) {
+ SourceDebugCache.put(name, classNode);
+ }
+
+ // noop
+ @Override public String getRefMapperConfig() { return null; }
+ @Override public boolean shouldApplyMixin(String targetClassName, String mixinClassName) { return true; }
+ @Override public void acceptTargets(Set<String> myTargets, Set<String> otherTargets) { }
+ @Override public List<String> getMixins() { return null; }
+ @Override public void preApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) { }
+ @Override public void postApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) { }
+ @Override public boolean checkActive(MixinEnvironment environment) { return true; }
+ @Override public void preApply(ITargetClassContext context) { }
+ @Override public void postApply(ITargetClassContext context) { }
+
+}
diff --git a/spark-fabric/src/main/java/me/lucko/spark/fabric/smap/MixinUtils.java b/spark-fabric/src/main/java/me/lucko/spark/fabric/smap/MixinUtils.java
new file mode 100644
index 0000000..ebf2766
--- /dev/null
+++ b/spark-fabric/src/main/java/me/lucko/spark/fabric/smap/MixinUtils.java
@@ -0,0 +1,52 @@
+/*
+ * 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.fabric.smap;
+
+import org.spongepowered.asm.mixin.transformer.Config;
+
+import java.lang.reflect.Field;
+import java.util.HashMap;
+import java.util.Map;
+
+public enum MixinUtils {
+ ;
+
+ private static final Map<String, Config> MIXIN_CONFIGS;
+
+ static {
+ Map<String, Config> configs;
+ try {
+ Field allConfigsField = Config.class.getDeclaredField("allConfigs");
+ allConfigsField.setAccessible(true);
+
+ //noinspection unchecked
+ configs = (Map<String, Config>) allConfigsField.get(null);
+ } catch (Exception e) {
+ e.printStackTrace();
+ configs = new HashMap<>();
+ }
+ MIXIN_CONFIGS = configs;
+ }
+
+ public static Map<String, Config> getMixinConfigs() {
+ return MIXIN_CONFIGS;
+ }
+}
diff --git a/spark-fabric/src/main/java/me/lucko/spark/fabric/smap/SourceDebugCache.java b/spark-fabric/src/main/java/me/lucko/spark/fabric/smap/SourceDebugCache.java
new file mode 100644
index 0000000..88adae6
--- /dev/null
+++ b/spark-fabric/src/main/java/me/lucko/spark/fabric/smap/SourceDebugCache.java
@@ -0,0 +1,87 @@
+/*
+ * 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.fabric.smap;
+
+import org.objectweb.asm.tree.ClassNode;
+import org.spongepowered.asm.service.IClassBytecodeProvider;
+import org.spongepowered.asm.service.MixinService;
+
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Caches the lookup of class -> source debug info for classes loaded on the JVM.
+ *
+ * The {@link me.lucko.spark.fabric.plugin.FabricSparkMixinPlugin} also supplements this cache with
+ * extra information as classes are exported.
+ */
+public enum SourceDebugCache {
+ ;
+
+ // class name -> smap
+ private static final Map<String, SmapValue> CACHE = new ConcurrentHashMap<>();
+
+ public static void put(String className, ClassNode node) {
+ if (className == null || node == null) {
+ return;
+ }
+ className = className.replace('/', '.');
+ CACHE.put(className, SmapValue.of(node.sourceDebug));
+ }
+
+ public static String getSourceDebugInfo(String className) {
+ SmapValue cached = CACHE.get(className);
+ if (cached != null) {
+ return cached.value();
+ }
+
+ try {
+ IClassBytecodeProvider provider = MixinService.getService().getBytecodeProvider();
+ ClassNode classNode = provider.getClassNode(className.replace('.', '/'));
+
+ if (classNode != null) {
+ put(className, classNode);
+ return classNode.sourceDebug;
+ }
+
+ } catch (Exception e) {
+ // ignore
+ }
+
+ CACHE.put(className, SmapValue.NULL);
+ return null;
+ }
+
+ private record SmapValue(String value) {
+ static final SmapValue NULL = new SmapValue(null);
+
+ static SmapValue of(String value) {
+ if (value == null) {
+ return NULL;
+ } else {
+ return new SmapValue(value);
+ }
+ }
+
+ }
+
+}
diff --git a/spark-fabric/src/main/java/me/lucko/spark/fabric/smap/SourceMap.java b/spark-fabric/src/main/java/me/lucko/spark/fabric/smap/SourceMap.java
new file mode 100644
index 0000000..5105a26
--- /dev/null
+++ b/spark-fabric/src/main/java/me/lucko/spark/fabric/smap/SourceMap.java
@@ -0,0 +1,133 @@
+/*
+ * SMAPSourceDebugExtension.java - Parse source debug extensions and
+ * enhance stack traces.
+ *
+ * Copyright (c) 2012 Michael Schierl
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * - Neither name of the copyright holders nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND THE CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDERS OR THE CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
+ * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
+ * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
+ * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package me.lucko.spark.fabric.smap;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Utility class to parse "SMAP" (source map) information from loaded Java classes.
+ *
+ * @author <a href="https://stackoverflow.com/a/11299757">Michael Schierl</a>
+ */
+public class SourceMap {
+
+ private final String generatedFileName;
+ private final String firstStratum;
+ private final Map<Integer, FileInfo> fileinfo = new HashMap<>();
+ private final Map<Integer, int[]> reverseLineMapping = new HashMap<>();
+
+ private static final Pattern LINE_INFO_PATTERN = Pattern.compile("([0-9]+)(?:#([0-9]+))?(?:,([0-9]+))?:([0-9]+)(?:,([0-9]+))?");
+
+ public SourceMap(String value) {
+ String[] lines = value.split("\n");
+ if (!lines[0].equals("SMAP") || !lines[3].startsWith("*S ") || !lines[4].equals("*F")) {
+ throw new IllegalArgumentException(value);
+ }
+
+ this.generatedFileName = lines[1];
+ this.firstStratum = lines[3].substring(3);
+
+ int idx = 5;
+ while (!lines[idx].startsWith("*")) {
+ String infoline = lines[idx++];
+ String path = null;
+
+ if (infoline.startsWith("+ ")) {
+ path = lines[idx++];
+ infoline = infoline.substring(2);
+ }
+
+ int pos = infoline.indexOf(" ");
+ int filenum = Integer.parseInt(infoline.substring(0, pos));
+ String name = infoline.substring(pos + 1);
+
+ this.fileinfo.put(filenum, new FileInfo(name, path == null ? name : path));
+ }
+
+ if (lines[idx].equals("*L")) {
+ idx++;
+ int lastLFI = 0;
+
+ while (!lines[idx].startsWith("*")) {
+ Matcher m = LINE_INFO_PATTERN.matcher(lines[idx++]);
+ if (!m.matches()) {
+ throw new IllegalArgumentException(lines[idx - 1]);
+ }
+
+ int inputStartLine = Integer.parseInt(m.group(1));
+ int lineFileID = m.group(2) == null ? lastLFI : Integer.parseInt(m.group(2));
+ int repeatCount = m.group(3) == null ? 1 : Integer.parseInt(m.group(3));
+ int outputStartLine = Integer.parseInt(m.group(4));
+ int outputLineIncrement = m.group(5) == null ? 1 : Integer.parseInt(m.group(5));
+
+ for (int i = 0; i < repeatCount; i++) {
+ int[] inputMapping = new int[] { lineFileID, inputStartLine + i };
+ int baseOL = outputStartLine + i * outputLineIncrement;
+
+ for (int ol = baseOL; ol < baseOL + outputLineIncrement; ol++) {
+ if (!this.reverseLineMapping.containsKey(ol)) {
+ this.reverseLineMapping.put(ol, inputMapping);
+ }
+ }
+ }
+
+ lastLFI = lineFileID;
+ }
+ }
+ }
+
+ public String getGeneratedFileName() {
+ return this.generatedFileName;
+ }
+
+ public String getFirstStratum() {
+ return this.firstStratum;
+ }
+
+ public Map<Integer, FileInfo> getFileInfo() {
+ return this.fileinfo;
+ }
+
+ public Map<Integer, int[]> getReverseLineMapping() {
+ return this.reverseLineMapping;
+ }
+
+ public record FileInfo(String name, String path) { }
+} \ No newline at end of file
diff --git a/spark-fabric/src/main/java/me/lucko/spark/fabric/smap/SourceMapProvider.java b/spark-fabric/src/main/java/me/lucko/spark/fabric/smap/SourceMapProvider.java
new file mode 100644
index 0000000..1a4f246
--- /dev/null
+++ b/spark-fabric/src/main/java/me/lucko/spark/fabric/smap/SourceMapProvider.java
@@ -0,0 +1,53 @@
+/*
+ * 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.fabric.smap;
+
+import org.jetbrains.annotations.Nullable;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class SourceMapProvider {
+ private final Map<String, SourceMap> cache = new HashMap<>();
+
+ public @Nullable SourceMap getSourceMap(String className) {
+ if (this.cache.containsKey(className)) {
+ return this.cache.get(className);
+ }
+
+ SourceMap smap = null;
+ try {
+ String value = SourceDebugCache.getSourceDebugInfo(className);
+ if (value != null) {
+ value = value.replaceAll("\r\n?", "\n");
+ if (value.startsWith("SMAP\n")) {
+ smap = new SourceMap(value);
+ }
+ }
+ } catch (Exception e) {
+ // ignore
+ }
+
+ this.cache.put(className, smap);
+ return smap;
+ }
+
+}
diff --git a/spark-fabric/src/main/resources/spark.mixins.json b/spark-fabric/src/main/resources/spark.mixins.json
index e75b34f..63c1078 100644
--- a/spark-fabric/src/main/resources/spark.mixins.json
+++ b/spark-fabric/src/main/resources/spark.mixins.json
@@ -11,5 +11,6 @@
"server": [
"ServerEntityManagerAccessor",
"ServerWorldAccessor"
- ]
+ ],
+ "plugin": "me.lucko.spark.fabric.plugin.FabricSparkMixinPlugin"
} \ No newline at end of file