Add mod scanning optimization (requires Blacksmith)
This commit is contained in:
parent
d9378d4a80
commit
48b4f976df
|
|
@ -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,
|
||||
|
|
|
|||
59
src/main/java/org/embeddedt/modernfix/agent/Agent.java
Normal file
59
src/main/java/org/embeddedt/modernfix/agent/Agent.java
Normal file
|
|
@ -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<String, Function<ClassNode, ClassNode>> TRANSFORMERS = ImmutableMap.<String, Function<ClassNode, ClassNode>>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<ClassNode, ClassNode> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Path> transformers = (List<Path>)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<String> otherServices) throws IncompatibleEnvironmentException {
|
||||
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public List<ITransformer> transformers() {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
|
@ -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<String, BiFunction<String, byte[], byte[]>> TRANSFORMER_MAP = ImmutableMap.<String, BiFunction<String, byte[], byte[]>>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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<ModFileScanData> 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<ModFileScanData> cir) {
|
||||
CachedScanner.saveCachedDataForFile(this.fileToScan, cir.getReturnValue());
|
||||
}
|
||||
}
|
||||
|
|
@ -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<String> interfacesTypeDesc;
|
||||
}
|
||||
|
||||
static class SerializedAnnotationData implements Serializable {
|
||||
public String annotationTypeDesc;
|
||||
public ElementType targetTypeDesc;
|
||||
public String classTypeDesc;
|
||||
public String memberName;
|
||||
public Map<String, Object> 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<SerializedClassData> classDataList = (ArrayList<SerializedClassData>)stream.readObject();
|
||||
Set<ModFileScanData.ClassData> 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<SerializedAnnotationData> annotationDataList = (ArrayList<SerializedAnnotationData>)stream.readObject();
|
||||
Set<ModFileScanData.AnnotationData> 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<SerializedClassData> 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<Type>)classDataInterfacesField.get(data)).stream().map(Type::getDescriptor).collect(Collectors.toCollection(ArrayList::new));
|
||||
serializedClassDataList.add(sData);
|
||||
}
|
||||
stream.writeObject(serializedClassDataList);
|
||||
ArrayList<SerializedAnnotationData> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user