Add mod scanning optimization (requires Blacksmith)

This commit is contained in:
embeddedt 2023-01-28 20:44:35 -05:00
parent d9378d4a80
commit 48b4f976df
No known key found for this signature in database
GPG Key ID: A69433EC199B5613
6 changed files with 60 additions and 311 deletions

View File

@ -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,

View 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;
}
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}
}

View File

@ -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());
}
}

View File

@ -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();
}
}
}