Add mod scanning optimization (requires Blacksmith)
This commit is contained in:
parent
d9378d4a80
commit
48b4f976df
|
|
@ -159,6 +159,7 @@ jar {
|
||||||
manifest {
|
manifest {
|
||||||
attributes([
|
attributes([
|
||||||
"Specification-Title" : "modernfix",
|
"Specification-Title" : "modernfix",
|
||||||
|
"Operative-Class" : "org.embeddedt.modernfix.agent.Agent",
|
||||||
//"Specification-Vendor": "modernfix authors",
|
//"Specification-Vendor": "modernfix authors",
|
||||||
"Specification-Version" : "1", // We are version 1 of ourselves
|
"Specification-Version" : "1", // We are version 1 of ourselves
|
||||||
"Implementation-Title" : project.name,
|
"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