From 48b4f976dfd9e0b7390fa8ddbd8ef50b0d3cbc4c Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Sat, 28 Jan 2023 20:44:35 -0500 Subject: [PATCH] Add mod scanning optimization (requires Blacksmith) --- build.gradle | 1 + .../org/embeddedt/modernfix/agent/Agent.java | 59 +++++++ .../service/EarlyLoadingService.java | 76 --------- .../ModernFixRetransformingClassLoader.java | 49 ------ .../mixin/perf/scan_cache/ScannerMixin.java | 29 ---- .../modernfix/scanning/CachedScanner.java | 157 ------------------ 6 files changed, 60 insertions(+), 311 deletions(-) create mode 100644 src/main/java/org/embeddedt/modernfix/agent/Agent.java delete mode 100644 src/main/java/org/embeddedt/modernfix/classloading/service/EarlyLoadingService.java delete mode 100644 src/main/java/org/embeddedt/modernfix/classloading/service/ModernFixRetransformingClassLoader.java delete mode 100644 src/main/java/org/embeddedt/modernfix/mixin/perf/scan_cache/ScannerMixin.java delete mode 100644 src/main/java/org/embeddedt/modernfix/scanning/CachedScanner.java diff --git a/build.gradle b/build.gradle index 26f3ac70..8b116bdb 100644 --- a/build.gradle +++ b/build.gradle @@ -159,6 +159,7 @@ jar { manifest { attributes([ "Specification-Title" : "modernfix", + "Operative-Class" : "org.embeddedt.modernfix.agent.Agent", //"Specification-Vendor": "modernfix authors", "Specification-Version" : "1", // We are version 1 of ourselves "Implementation-Title" : project.name, diff --git a/src/main/java/org/embeddedt/modernfix/agent/Agent.java b/src/main/java/org/embeddedt/modernfix/agent/Agent.java new file mode 100644 index 00000000..5cf5298d --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/agent/Agent.java @@ -0,0 +1,59 @@ +package org.embeddedt.modernfix.agent; + +import com.google.common.collect.ImmutableMap; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.tree.*; + +import java.lang.instrument.ClassFileTransformer; +import java.lang.instrument.IllegalClassFormatException; +import java.lang.instrument.Instrumentation; +import java.security.ProtectionDomain; +import java.util.function.Function; + +public class Agent { + public static void agentmain(String args, Instrumentation instrumentation) { + instrumentation.addTransformer(new EarlyTransformer()); + } + + private static class EarlyTransformer implements ClassFileTransformer { + + private static final ImmutableMap> TRANSFORMERS = ImmutableMap.>builder() + .put("net/minecraftforge/fml/loading/moddiscovery/Scanner", EarlyTransformer::transformScanner) + .build(); + + private static ClassNode transformScanner(ClassNode input) { + for(MethodNode method : input.methods) { + if(method.name.equals("fileVisitor")) { + for(int i = 0; i < method.instructions.size(); i++) { + AbstractInsnNode ainsn = method.instructions.get(i); + if(ainsn.getOpcode() == Opcodes.INVOKEVIRTUAL) { + MethodInsnNode minsn = (MethodInsnNode)ainsn; + if(minsn.name.equals("accept") && minsn.owner.equals("org/objectweb/asm/ClassReader")) { + method.instructions.set(minsn.getPrevious(), new LdcInsnNode(ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES)); + return input; + } + } + } + } + } + return input; + } + + @Override + public byte[] transform(ClassLoader classLoader, String s, Class aClass, ProtectionDomain protectionDomain, byte[] bytes) throws IllegalClassFormatException { + Function func = TRANSFORMERS.get(s); + if(func != null) { + ClassReader reader = new ClassReader(bytes); + ClassNode node = new ClassNode(Opcodes.ASM9); + reader.accept(node, 0); + node = func.apply(node); + ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS); + node.accept(writer); + return writer.toByteArray(); + } else + return bytes; + } + } +} diff --git a/src/main/java/org/embeddedt/modernfix/classloading/service/EarlyLoadingService.java b/src/main/java/org/embeddedt/modernfix/classloading/service/EarlyLoadingService.java deleted file mode 100644 index edcfa37d..00000000 --- a/src/main/java/org/embeddedt/modernfix/classloading/service/EarlyLoadingService.java +++ /dev/null @@ -1,76 +0,0 @@ -package org.embeddedt.modernfix.classloading.service; - -import cpw.mods.modlauncher.ArgumentHandler; -import cpw.mods.modlauncher.Launcher; -import cpw.mods.modlauncher.api.IEnvironment; -import cpw.mods.modlauncher.api.ITransformationService; -import cpw.mods.modlauncher.api.ITransformer; -import cpw.mods.modlauncher.api.IncompatibleEnvironmentException; -import net.minecraftforge.fml.common.ObfuscationReflectionHelper; -import net.minecraftforge.fml.loading.ModDirTransformerDiscoverer; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import javax.annotation.Nonnull; -import java.lang.reflect.Field; -import java.lang.reflect.Method; -import java.nio.file.Path; -import java.util.Collections; -import java.util.List; -import java.util.Set; - -/** - * Used as a hook to class load on the ModLauncher class loader. - * - * We also need to ensure the ModernFix JAR is removed from the exclusions list, to ensure it loads as a regular mod. - */ -public class EarlyLoadingService implements ITransformationService { - private static final Logger LOGGER = LogManager.getLogger("ModernFixEarlyLoadingService"); - public Class cachingTransformerClass; - @Nonnull - @Override - public String name() { - return "modernfix"; - } - - public EarlyLoadingService() { - LOGGER.info("ModernFix (very) early loading"); - - - try { - ClassLoader loader = EarlyLoadingService.class.getClassLoader(); - Class.forName("cpw.mods.modlauncher.ClassTransformer", true, loader); - /* Allow ModernFix to be scanned like a mod */ - Field transformersField = ModDirTransformerDiscoverer.class.getDeclaredField("transformers"); - transformersField.setAccessible(true); - List transformers = (List)transformersField.get(null); - transformers.removeIf(path -> path.toString().contains("modernfix")); - - /* Load our new transformer */ - cachingTransformerClass = Class.forName("cpw.mods.modlauncher.ModernFixCachingClassTransformer", true, loader); - } catch(ReflectiveOperationException e) { - throw new RuntimeException(e); - } - } - - @Override - public void initialize(IEnvironment environment) { - - } - - @Override - public void beginScanning(IEnvironment environment) { - - } - - @Override - public void onLoad(IEnvironment env, Set otherServices) throws IncompatibleEnvironmentException { - - } - - @Nonnull - @Override - public List transformers() { - return Collections.emptyList(); - } -} diff --git a/src/main/java/org/embeddedt/modernfix/classloading/service/ModernFixRetransformingClassLoader.java b/src/main/java/org/embeddedt/modernfix/classloading/service/ModernFixRetransformingClassLoader.java deleted file mode 100644 index 27c17ca4..00000000 --- a/src/main/java/org/embeddedt/modernfix/classloading/service/ModernFixRetransformingClassLoader.java +++ /dev/null @@ -1,49 +0,0 @@ -package org.embeddedt.modernfix.classloading.service; - -import com.google.common.collect.ImmutableMap; -import com.google.common.io.Resources; -import cpw.mods.gross.Java9ClassLoaderUtil; - -import java.io.IOException; -import java.net.URLClassLoader; -import java.util.function.BiFunction; -import java.util.function.Function; - -/** - * This becomes the new "system" classloader and is used to retransform ModLauncher as needed. - */ -public class ModernFixRetransformingClassLoader extends URLClassLoader { - private static final ImmutableMap> TRANSFORMER_MAP = ImmutableMap.>builder() - .put("cpw.mods.modlauncher.Launcher", ModernFixRetransformingClassLoader::transformModLauncher) - .build(); - - static { - ClassLoader.registerAsParallelCapable(); - } - private final ClassLoader resourceFinder; - public ModernFixRetransformingClassLoader(ClassLoader resourceFinder) { - super(Java9ClassLoaderUtil.getSystemClassPathURLs(), null); - this.resourceFinder = resourceFinder; - } - - private static byte[] transformModLauncher(String s, byte[] in) { - return in; - } - - @Override - public Class loadClass(String s) throws ClassNotFoundException { - synchronized(this.getClassLoadingLock(s)) { - if(!TRANSFORMER_MAP.containsKey(s)) { - return super.loadClass(s); - } - byte[] classBytes; - try { - classBytes = Resources.toByteArray(this.resourceFinder.getResource(s.replace('.', '/') + ".class")); - } catch(IOException e) { - throw new ClassNotFoundException("Failed to load class bytes", e); - } - byte[] transformed = TRANSFORMER_MAP.get(s).apply(s, classBytes); - return defineClass(s, transformed, 0, transformed.length); - } - } -} diff --git a/src/main/java/org/embeddedt/modernfix/mixin/perf/scan_cache/ScannerMixin.java b/src/main/java/org/embeddedt/modernfix/mixin/perf/scan_cache/ScannerMixin.java deleted file mode 100644 index b49602c7..00000000 --- a/src/main/java/org/embeddedt/modernfix/mixin/perf/scan_cache/ScannerMixin.java +++ /dev/null @@ -1,29 +0,0 @@ -package org.embeddedt.modernfix.mixin.perf.scan_cache; - -import net.minecraftforge.fml.loading.moddiscovery.ModFile; -import net.minecraftforge.fml.loading.moddiscovery.Scanner; -import net.minecraftforge.forgespi.language.ModFileScanData; -import org.embeddedt.modernfix.scanning.CachedScanner; -import org.spongepowered.asm.mixin.Final; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Shadow; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; - -@Mixin(Scanner.class) -public class ScannerMixin { - @Shadow(remap = false) @Final private ModFile fileToScan; - - @Inject(method = "scan", at = @At(value = "HEAD"), cancellable = true, remap = false) - private void useCachedScanResults(CallbackInfoReturnable cir) { - ModFileScanData cached = CachedScanner.getCachedDataForFile(this.fileToScan); - if(cached != null) - cir.setReturnValue(cached); - } - - @Inject(method = "scan", at = @At(value = "TAIL"), remap = false) - private void saveCachedScanResults(CallbackInfoReturnable cir) { - CachedScanner.saveCachedDataForFile(this.fileToScan, cir.getReturnValue()); - } -} diff --git a/src/main/java/org/embeddedt/modernfix/scanning/CachedScanner.java b/src/main/java/org/embeddedt/modernfix/scanning/CachedScanner.java deleted file mode 100644 index 1b1c8300..00000000 --- a/src/main/java/org/embeddedt/modernfix/scanning/CachedScanner.java +++ /dev/null @@ -1,157 +0,0 @@ -package org.embeddedt.modernfix.scanning; - -import cpw.mods.modlauncher.api.LamdbaExceptionUtils; -import net.minecraftforge.fml.loading.FMLPaths; -import net.minecraftforge.fml.loading.moddiscovery.ModFile; -import net.minecraftforge.forgespi.language.IModLanguageProvider; -import net.minecraftforge.forgespi.language.ModFileScanData; -import org.objectweb.asm.Type; - -import java.io.*; -import java.lang.annotation.ElementType; -import java.lang.reflect.Field; -import java.nio.file.Files; -import java.nio.file.Path; -import java.security.MessageDigest; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Map; -import java.util.Set; -import java.util.stream.Collectors; - -public class CachedScanner { - private static final Path SCAN_CACHE_FOLDER = FMLPaths.GAMEDIR.get().resolve("modernfix").resolve("scanCacheV1"); - - private static File getCacheFileLocation(ModFile file) { - Path modPath = FMLPaths.MODSDIR.get().relativize(file.getFilePath()); - return SCAN_CACHE_FOLDER.resolve(modPath).toFile(); - } - - private static MessageDigest modFileDigest = LamdbaExceptionUtils.uncheck(() -> MessageDigest.getInstance("SHA-256")); - - private static byte[] computeModFileHash(ModFile file) { - modFileDigest.reset(); - byte[] buffer = new byte[8192]; - int bytesRead; - try(BufferedInputStream stream = new BufferedInputStream(Files.newInputStream(file.getFilePath()))) { - while((bytesRead = stream.read(buffer, 0, buffer.length)) > 0) { - modFileDigest.update(buffer, 0, bytesRead); - } - } catch(IOException e) { - throw new RuntimeException("Failed to read mod file", e); - } - return modFileDigest.digest(); - } - - private static String getCurrentLangVersion(ModFile file) { - IModLanguageProvider loader = file.getLoader(); - String currentLangVersion = loader.getClass().getPackage().getImplementationVersion(); - if(currentLangVersion == null) - currentLangVersion = "[none]"; - return currentLangVersion; - } - - static class SerializedClassData implements Serializable { - public String classTypeDesc; - public String parentTypeDesc; - public ArrayList interfacesTypeDesc; - } - - static class SerializedAnnotationData implements Serializable { - public String annotationTypeDesc; - public ElementType targetTypeDesc; - public String classTypeDesc; - public String memberName; - public Map annotationData; - } - - private static ModFileScanData deserializeScanData(ModFile file, ObjectInputStream stream) throws IOException, ClassNotFoundException { - ModFileScanData result = new ModFileScanData(); - result.addModFileInfo(file.getModFileInfo()); - /* Read all the classes */ - ArrayList classDataList = (ArrayList)stream.readObject(); - Set classDataSet = result.getClasses(); - for(SerializedClassData data : classDataList) { - classDataSet.add(new ModFileScanData.ClassData( - Type.getType(data.classTypeDesc), - Type.getType(data.parentTypeDesc), - data.interfacesTypeDesc.stream().map(Type::getType).collect(Collectors.toSet()))); - } - /* Read all the annotations */ - ArrayList annotationDataList = (ArrayList)stream.readObject(); - Set annotationDataSet = result.getAnnotations(); - for(SerializedAnnotationData data : annotationDataList) { - annotationDataSet.add(new ModFileScanData.AnnotationData( - Type.getType(data.annotationTypeDesc), - data.targetTypeDesc, - Type.getType(data.classTypeDesc), - data.memberName, - data.annotationData - )); - } - return result; - } - - public static ModFileScanData getCachedDataForFile(ModFile file) { - byte[] currentHash = computeModFileHash(file); - String currentLangVersion = getCurrentLangVersion(file); - try(ObjectInputStream stream = new ObjectInputStream(new FileInputStream(getCacheFileLocation(file)))) { - byte[] modFileHash = (byte[])stream.readObject(); - if(!Arrays.equals(modFileHash, currentHash)) { - return null; - } - String langVersion = stream.readUTF(); - if(!langVersion.equals(currentLangVersion)) - return null; - return deserializeScanData(file, stream); - } catch(IOException | ClassNotFoundException e) { - if(!(e instanceof FileNotFoundException)) - e.printStackTrace(); - return null; - } - } - - private static Field classDataTypeField, classDataParentField, classDataInterfacesField; - - public static void saveCachedDataForFile(ModFile file, ModFileScanData scanData) { - try(ObjectOutputStream stream = new ObjectOutputStream(new FileOutputStream(getCacheFileLocation(file)))) { - stream.writeObject(computeModFileHash(file)); - stream.writeObject(getCurrentLangVersion(file)); - /* serialize scan data */ - ArrayList serializedClassDataList = new ArrayList<>(); - for(ModFileScanData.ClassData data : scanData.getClasses()) { - SerializedClassData sData = new SerializedClassData(); - if(classDataTypeField == null) { - classDataTypeField = ModFileScanData.ClassData.class.getDeclaredField("clazz"); - classDataTypeField.setAccessible(true); - } - sData.classTypeDesc = ((Type)classDataTypeField.get(data)).getDescriptor(); - if(classDataTypeField == null) { - classDataTypeField = ModFileScanData.ClassData.class.getDeclaredField("clazz"); - classDataTypeField.setAccessible(true); - } - sData.classTypeDesc = ((Type)classDataTypeField.get(data)).getDescriptor(); - if(classDataInterfacesField == null) { - classDataInterfacesField = ModFileScanData.ClassData.class.getDeclaredField("interfaces"); - classDataInterfacesField.setAccessible(true); - } - sData.interfacesTypeDesc = ((Set)classDataInterfacesField.get(data)).stream().map(Type::getDescriptor).collect(Collectors.toCollection(ArrayList::new)); - serializedClassDataList.add(sData); - } - stream.writeObject(serializedClassDataList); - ArrayList serializedAnnotationDataList = new ArrayList<>(); - for(ModFileScanData.AnnotationData data : scanData.getAnnotations()) { - SerializedAnnotationData sData = new SerializedAnnotationData(); - sData.annotationTypeDesc = data.getAnnotationType().getDescriptor(); - sData.targetTypeDesc = data.getTargetType(); - sData.classTypeDesc = data.getClassType().getDescriptor(); - sData.memberName = data.getMemberName(); - sData.annotationData = data.getAnnotationData(); - serializedAnnotationDataList.add(sData); -; } - stream.writeObject(serializedAnnotationDataList); - } catch(IOException | ReflectiveOperationException e) { - e.printStackTrace(); - } - } -}