diff options
Diffstat (limited to 'spark-fabric/src/main')
7 files changed, 551 insertions, 7 deletions
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 |