New class transformer cache

Hashes individual mixins/coremods and therefore needs no special invalidation logic
This commit is contained in:
embeddedt 2023-01-21 12:33:17 -05:00
parent 62eb72ea68
commit 41eef0b6ab
No known key found for this signature in database
GPG Key ID: A69433EC199B5613
5 changed files with 313 additions and 226 deletions

View File

@ -1,51 +1,45 @@
package cpw.mods.modlauncher;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.EOFException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.BlockingQueue;
import java.io.*;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.LinkedBlockingQueue;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.io.Files;
import cpw.mods.modlauncher.api.ITransformer;
import cpw.mods.modlauncher.api.ITransformerActivity;
import cpw.mods.modlauncher.serviceapi.ILaunchPluginService;
import net.minecraftforge.coremod.transformer.CoreModBaseTransformer;
import net.minecraftforge.fml.loading.FMLPaths;
import net.minecraftforge.fml.loading.LoadingModList;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.embeddedt.modernfix.ModernFix;
import org.embeddedt.modernfix.classloading.api.IHashableTransformer;
import org.embeddedt.modernfix.classloading.hashers.CoreModTransformerHasher;
import org.embeddedt.modernfix.classloading.hashers.MixinTransformerHasher;
import org.objectweb.asm.Type;
import org.spongepowered.asm.launch.MixinLaunchPluginLegacy;
import javax.lang.model.SourceVersion;
public class ModernFixCachingClassTransformer extends ClassTransformer {
private static final Logger LOGGER = LogManager.getLogger("ModernFixCachingTransformer");
private Map<String, Optional<byte[]>> cache = new ConcurrentHashMap<>();
private final static int QUEUE_SIZE = 512; // Config.recentCacheSize;
Optional<Cache<String, byte[]>> recentCache = QUEUE_SIZE < 0 ? Optional.empty() :
Optional.of(CacheBuilder.newBuilder().maximumSize(QUEUE_SIZE).build());
private static final boolean FORCE_REBUILD_CACHE = Boolean.parseBoolean(System.getProperty("coretweaks.transformerCache.full.forceRebuild", "false"));
private static final File CLASS_CACHE_DAT = childFile(FMLPaths.GAMEDIR.get().resolve("modernfix").resolve("classTransformerV1.cache").toFile());
private final LaunchPluginHandler pluginHandler;
private final TransformStore transformStore;
private final TransformerAuditTrail auditTrail;
private final TransformingClassLoader transformingClassLoader;
private final HashMap<String, List<ITransformer<?>>> transformersByClass;
public static final boolean DEBUG_PRINT = true;
private static final int MAX_NUM_CLASSES = 10000;
private int lastSaveSize = 0;
private BlockingQueue<String> dirtyClasses = new LinkedBlockingQueue<String>();
private SaveThread saveThread = new SaveThread(this);
private static final File CLASS_CACHE_DAT = childFile(FMLPaths.GAMEDIR.get().resolve("modernfix").resolve("classTransformerFull.cache").toFile());
private static final File CLASS_CACHE_DAT_ERRORED = childFile(FMLPaths.GAMEDIR.get().resolve("modernfix").resolve("classTransformerFull.cache.errored").toFile());
private static final File CLASS_CACHE_DAT_TMP = childFile(FMLPaths.GAMEDIR.get().resolve("modernfix").resolve("classTransformerFull.cache~").toFile());
private ConcurrentHashMap<String, Pair<List<byte[]>, byte[]>> transformationCache;
private static File childFile(File file) {
file.getParentFile().mkdirs();
@ -60,216 +54,143 @@ public class ModernFixCachingClassTransformer extends ClassTransformer {
return SourceVersion.isName(className);
}
static class SaveThread extends Thread {
private ModernFixCachingClassTransformer cacheTransformer;
private int saveInterval = 10000;
public SaveThread(ModernFixCachingClassTransformer ct) {
this.cacheTransformer = ct;
setName("CacheTransformer save thread");
setDaemon(false);
}
@Override
public void run() {
while(true) {
try {
Thread.sleep(saveInterval);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
cacheTransformer.doSave();
}
}
}
public ModernFixCachingClassTransformer(TransformStore transformStore, LaunchPluginHandler pluginHandler, TransformingClassLoader transformingClassLoader, TransformerAuditTrail trail) {
super(transformStore, pluginHandler, transformingClassLoader, trail);
if(FORCE_REBUILD_CACHE) {// || Persistence.modsChanged()) {
clearCache(FORCE_REBUILD_CACHE ? "forceRebuild JVM flag was set." : "mods have changed.");
} else {
loadCache();
}
saveThread.start();
}
private void clearCache(String reason) {
LOGGER.info("Rebuilding class cache, because " + reason);
CLASS_CACHE_DAT.delete();
}
public void doSave() {
saveCache();
}
private void loadCache() {
File inFile = CLASS_CACHE_DAT;
if(inFile.exists()) {
LOGGER.info("Loading class cache.");
cache.clear();
try (DataInputStream in = new DataInputStream(new BufferedInputStream(new FileInputStream(inFile)))){
try {
while(true) { // EOFException should break the loop
String className = in.readUTF();
int classLength = in.readInt();
byte[] classData = new byte[classLength];
int bytesRead = in.read(classData, 0, classLength);
if(!isValidClassName(className)) {
throw new RuntimeException("Invalid class name: " + className);
} else if(bytesRead != classLength) {
throw new RuntimeException("Length of " + className + " doesn't match advertised length of " + classLength);
} else {
cache.put(className, Optional.of(classData));
superDebug("Loaded " + className);
}
}
} catch(EOFException eof) {}
} catch (Exception e) {
LOGGER.error("There was an error reading the transformer cache. A new one will be created. The previous one has been saved as " + CLASS_CACHE_DAT_ERRORED.getName() + " for inspection.");
CLASS_CACHE_DAT.renameTo(CLASS_CACHE_DAT_ERRORED);
e.printStackTrace();
cache.clear();
}
LOGGER.info("Loaded " + cache.size() + " cached classes.");
lastSaveSize = cache.size();
} else {
LOGGER.info("Couldn't find class cache file");
}
}
private void saveCacheFully() {
File outFile = CLASS_CACHE_DAT;
File outFileTmp = CLASS_CACHE_DAT_TMP;
LOGGER.info("Performing full save of class cache (size: " + cache.size() + ")");
saveCacheChunk(cache.keySet(), outFileTmp, false);
this.transformStore = transformStore;
this.pluginHandler = pluginHandler;
this.transformingClassLoader = transformingClassLoader;
this.auditTrail = trail;
/* Build a lookup table of all transformers for a given class */
this.transformersByClass = new HashMap<>();
try {
Files.move(outFileTmp, outFile);
} catch (IOException e) {
LOGGER.error("Failed to finish saving class cache");
e.printStackTrace();
}
}
private void saveCache() {
if(dirtyClasses.isEmpty()) {
return; // don't save if the cache hasn't changed
}
File outFile = CLASS_CACHE_DAT;
try {
outFile.createNewFile();
} catch (IOException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
List<String> classesToSave = new ArrayList<String>();
dirtyClasses.drainTo(classesToSave);
if(DEBUG_PRINT) {
LOGGER.info("Saving class cache (size: " + lastSaveSize + " -> " + cache.size() + " | +" + classesToSave.size() + ")");
}
saveCacheChunk(classesToSave, outFile, true);
lastSaveSize += classesToSave.size();
}
private void saveCacheChunk(Collection<String> classesToSave, File outFile, boolean append) {
try(DataOutputStream out = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(outFile, append)))){
for(String name : classesToSave) {
Optional<byte[]> data = cache.get(name);
if(data != null && data.isPresent()) {
out.writeUTF(name);
out.writeInt(data.get().length);
out.write(data.get());
Field transformersByTypeField = TransformStore.class.getDeclaredField("transformers");
transformersByTypeField.setAccessible(true);
Field transformersMapField = TransformList.class.getDeclaredField("transformers");
transformersMapField.setAccessible(true);
EnumMap<TransformTargetLabel.LabelType, TransformList<?>> transformersByType = (EnumMap<TransformTargetLabel.LabelType, TransformList<?>>)transformersByTypeField.get(this.transformStore);
for(TransformList<?> transformList : transformersByType.values()) {
Map<TransformTargetLabel, List<ITransformer<?>>> transformers = (Map<TransformTargetLabel, List<ITransformer<?>>>)transformersMapField.get(transformList);
for(Map.Entry<TransformTargetLabel, List<ITransformer<?>>> entry : transformers.entrySet()) {
String className = entry.getKey().getClassName().getClassName();
List<ITransformer<?>> transformerList = this.transformersByClass.computeIfAbsent(className, k -> new ArrayList<>());
transformerList.addAll(entry.getValue());
}
}
if(DEBUG_PRINT) {
LOGGER.info("Saved class cache");
for(List<ITransformer<?>> transformerList : this.transformersByClass.values()) {
transformerList.sort((t1, t2) -> Comparator.<String>naturalOrder().compare(StringUtils.join(t1.labels(), " "), StringUtils.join(t2.labels(), " ")));
}
} catch (IOException e) {
LOGGER.info("Exception saving class cache");
// TODO Auto-generated catch block
e.printStackTrace();
} catch(ReflectiveOperationException e) {
throw new RuntimeException(e);
}
this.transformationCache = new ConcurrentHashMap<>();
try(ObjectInputStream inStream = new ObjectInputStream(new FileInputStream(CLASS_CACHE_DAT))) {
this.transformationCache = (ConcurrentHashMap<String, Pair<List<byte[]>,byte[]>>)inStream.readObject();
int size = 0;
/* Approximate the size in bytes */
for(Map.Entry<String, Pair<List<byte[]>,byte[]>> entry : this.transformationCache.entrySet()) {
size += entry.getKey().length();
size += entry.getValue().getRight().length;
for(byte[] hash : entry.getValue().getLeft()) {
size += hash.length;
}
}
LOGGER.info("Loaded transformer cache, contains " + this.transformationCache.size() + " classes and in-memory size is approximately " + FileUtils.byteCountToDisplaySize(size));
} catch(IOException | ClassNotFoundException e) {
LOGGER.error("An error occured while loading transform cache", e);
}
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
LOGGER.info("Serializing transform cache to disk");
try(ObjectOutputStream outStream = new ObjectOutputStream(new FileOutputStream(CLASS_CACHE_DAT))) {
outStream.writeObject(transformationCache);
} catch(IOException e) {
LOGGER.error("An error occured while serializing transform cache", e);
}
}, "ModernFix transformer shutdown thread"));
}
private String describeBytecode(byte[] basicClass) {
return basicClass == null ? "null" : String.format("length: %d, hash: %x", basicClass.length, basicClass.hashCode());
}
/**
* Check the hashed list of transformers and use a cached version of the class if possible. This code needs
* to be very fast as the entire point is to spend very little time doing transformation work that was done before.
* @param inputClass The bytecode to be transformed
* @param className Name of the class
* @param reason Reason for the class being loaded
* @return The transformed version of the class
*/
@Override
public byte[] transform(byte[] basicClass, String transformedName, String reason) {
public byte[] transform(byte[] inputClass, String className, String reason) {
/* We only want to cache actual transformations */
if(!ITransformerActivity.CLASSLOADING_REASON.equals(reason) || basicClass.length == 0) {
return super.transform(basicClass, transformedName, reason);
if(!ITransformerActivity.CLASSLOADING_REASON.equals(reason)) {
return super.transform(inputClass, className, reason);
}
byte[] result = null;
String name = transformedName;
try {
boolean dontCache = false;
/*
for(String badPrefix : badClasses) {
if(transformedName.startsWith(badPrefix)){
dontCache = true;
break;
final String internalName = className.replace('.', '/');
final Type classDesc = Type.getObjectType(internalName);
final EnumMap<ILaunchPluginService.Phase, List<ILaunchPluginService>> launchPluginTransformerSet = pluginHandler.computeLaunchPluginTransformerSet(classDesc, false, reason, this.auditTrail);
final boolean needsTransforming = transformStore.needsTransforming(internalName);
if (!needsTransforming && launchPluginTransformerSet.isEmpty()) {
return inputClass;
}
/* Now compute the hash list for the required transformers */
ArrayList<byte[]> hashList = new ArrayList<>();
for(List<ILaunchPluginService> pluginList : launchPluginTransformerSet.values()) {
pluginList.sort((service1, service2) -> Comparator.<String>naturalOrder().compare(service1.name(), service2.name()));
for(ILaunchPluginService service : pluginList) {
byte[] hash = obtainHash(service, className);
if(hash == null) {
return super.transform(inputClass, className, reason);
}
hashList.add(hash);
}
}
if(needsTransforming) {
List<ITransformer<?>> transformers = this.transformersByClass.get(internalName);
if(transformers != null) {
for(ITransformer<?> transformer : transformers) {
byte[] hash = obtainHash(transformer, className);
if(hash == null) {
return super.transform(inputClass, className, reason);
}
hashList.add(hash);
}
}
*/
if(cache.containsKey(transformedName) && !dontCache) {
if(cache.get(transformedName).isPresent()) { // we still remember it
result = cache.get(transformedName).get();
if(recentCache.isPresent()) {
// classes are only loaded once, so no need to keep it around in RAM
cache.put(transformedName, Optional.empty());
// but keep it around in case it's needed again by another transformer in the chain
recentCache.get().put(transformedName, result);
}
} else if(recentCache.isPresent()){ // we have forgotten it, hopefully it's still around in the recent queue
result = recentCache.get().getIfPresent(transformedName);
if(result == null) {
LOGGER.warn("Couldn't find " + transformedName + " in cache. Is recent queue too small? (" + QUEUE_SIZE + ")");
}
/* Check if the cache contains a transformed class matching these hashes */
return transformationCache.compute(className, (name, oldPair) -> {
boolean hashesMatch = true;
if(oldPair == null || oldPair.getLeft().size() != hashList.size()) {
hashesMatch = false;
} else {
for(int i = 0; i < oldPair.getLeft().size(); i++) {
if(!Arrays.equals(oldPair.getLeft().get(i), hashList.get(i))) {
hashesMatch = false;
}
}
}
if(result == null){
basicClass = super.transform(basicClass, transformedName, reason);
if(basicClass != null && !dontCache) {
cache.put(transformedName, Optional.of(basicClass)); // then cache it
dirtyClasses.add(transformedName);
}
result = basicClass;
if(hashesMatch)
return oldPair;
else {
if(oldPair != null) {
LOGGER.warn("Hashes have changed, discarding cached version of " + name);
}
byte[] transformed = super.transform(inputClass, name, reason);
return Pair.of(hashList, transformed);
}
if(result != null && recentCache.isPresent() && !dontCache) {
recentCache.get().put(transformedName, result);
}
} catch(Exception e) {
throw e; // pass it to LaunchClassLoader, who will handle it
} finally {
//wrappedTransformers.alt = this;
}
return result;
}).getRight();
}
private void superDebug(String msg) {
if(DEBUG_PRINT) {
LOGGER.debug(msg);
private static final byte[] FORGE_HASH = LoadingModList.get().getModFileById("forge").getMods().get(0).getVersion().toString().getBytes(StandardCharsets.UTF_8);
private byte[] obtainHash(Object o, String className) {
if(o instanceof CoreModBaseTransformer) {
return CoreModTransformerHasher.obtainHash((CoreModBaseTransformer<?>)o);
} else if(o instanceof MixinLaunchPluginLegacy) {
return MixinTransformerHasher.obtainHash((MixinLaunchPluginLegacy)o, className);
} else if(o instanceof IHashableTransformer) {
return ((IHashableTransformer)o).getHashForClass(className);
} else if(o.getClass().getName().startsWith("net.minecraftforge.")) {
return FORGE_HASH;
} else {
LOGGER.warn("No hash implementation found for: " + o.getClass().getName());
return null;
}
}
}

View File

@ -0,0 +1,11 @@
package org.embeddedt.modernfix.classloading.api;
public interface IHashableTransformer {
/**
* Called on an ILaunchPluginService or ITransformer to obtain a unique hash of the transformations that will be applied.
* Used to invalidate the transformation cache when needed.
* @param className Name of class being transformed
* @return A unique hash of the transformations that will be applied
*/
byte[] getHashForClass(String className);
}

View File

@ -0,0 +1,56 @@
package org.embeddedt.modernfix.classloading.hashers;
import net.minecraftforge.coremod.CoreMod;
import net.minecraftforge.coremod.transformer.CoreModBaseTransformer;
import java.io.IOException;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.concurrent.ConcurrentHashMap;
public class CoreModTransformerHasher {
private static final ConcurrentHashMap<CoreMod, byte[]> hashForCoremod;
private static Field coremodField;
private static ThreadLocal<MessageDigest> coremodHasher = ThreadLocal.withInitial(() -> {
try {
return MessageDigest.getInstance("SHA-256");
} catch(NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
});
static {
hashForCoremod = new ConcurrentHashMap<>();
try {
coremodField = CoreModBaseTransformer.class.getDeclaredField("coreMod");
coremodField.setAccessible(true);
} catch(ReflectiveOperationException e) {
throw new RuntimeException(e);
}
}
private static byte[] hashCoreMod(CoreMod coreMod) {
byte[] coreModContents;
try {
coreModContents = Files.readAllBytes(coreMod.getPath());
} catch(IOException e) {
throw new RuntimeException(e);
}
MessageDigest hasher = coremodHasher.get();
byte[] hash = hasher.digest(coreModContents);
hasher.reset();
return hash;
}
public static byte[] obtainHash(CoreModBaseTransformer<?> transformer) {
CoreMod coremod;
try {
coremod = (CoreMod)coremodField.get(transformer);
} catch(ReflectiveOperationException e) {
throw new RuntimeException(e);
}
return hashForCoremod.computeIfAbsent(coremod, CoreModTransformerHasher::hashCoreMod);
}
}

View File

@ -0,0 +1,97 @@
package org.embeddedt.modernfix.classloading.hashers;
import com.google.common.collect.HashMultimap;
import com.google.common.io.Resources;
import org.spongepowered.asm.launch.IClassProcessor;
import org.spongepowered.asm.launch.MixinLaunchPluginLegacy;
import org.spongepowered.asm.mixin.extensibility.IMixinInfo;
import org.spongepowered.asm.service.MixinService;
import java.io.IOException;
import java.lang.reflect.Field;
import java.net.URL;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.*;
public class MixinTransformerHasher {
private static HashMap<String, byte[]> hashesByClass = null;
private final static MessageDigest hasher;
static {
try {
hasher = MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
public static byte[] obtainHash(MixinLaunchPluginLegacy plugin, String className) {
synchronized (MixinTransformerHasher.class) {
if(hashesByClass == null) {
hashesByClass = new HashMap<>();
HashMap<String, ArrayList<IMixinInfo>> mixinsByClass = new HashMap<>();
try {
Field processorsField = MixinLaunchPluginLegacy.class.getDeclaredField("processors");
processorsField.setAccessible(true);
List<IClassProcessor> processors = (List<IClassProcessor>)processorsField.get(plugin);
Object transformHandler = null;
for(IClassProcessor processor : processors) {
if(processor.getClass().getName().equals("org.spongepowered.asm.service.modlauncher.MixinTransformationHandler")) {
transformHandler = processor;
break;
}
}
if(transformHandler == null)
throw new IllegalStateException("Mixin transform handler not found");
Field transformerField = transformHandler.getClass().getDeclaredField("transformer");
transformerField.setAccessible(true);
Object transformer = transformerField.get(transformHandler);
Field processorField = transformer.getClass().getDeclaredField("processor");
processorField.setAccessible(true);
Object processor = processorField.get(transformer);
Field configsField = processor.getClass().getDeclaredField("configs");
configsField.setAccessible(true);
List<?> configs = (List<?>)configsField.get(processor);
Field mixinsField = Class.forName("org.spongepowered.asm.mixin.transformer.MixinConfig").getDeclaredField("mixins");
mixinsField.setAccessible(true);
/* getTargetClasses can't be used because it's package-private */
Field classNamesField = Class.forName("org.spongepowered.asm.mixin.transformer.MixinInfo").getDeclaredField("targetClassNames");
classNamesField.setAccessible(true);
for(Object config : configs) {
List<? extends IMixinInfo> mixins = (List<? extends IMixinInfo>)mixinsField.get(config);
for(IMixinInfo mixin : mixins) {
List<String> targetClassNames = (List<String>)classNamesField.get(mixin);
for(String s : targetClassNames) {
mixinsByClass.computeIfAbsent(s, k -> new ArrayList<>()).add(mixin);
}
}
}
for(ArrayList<IMixinInfo> infos : mixinsByClass.values()) {
infos.sort((info1, info2) -> Comparator.<String>naturalOrder().compare(info1.getClassName(), info2.getClassName()));
}
/* Now go through each class name and hash it */
for(Map.Entry<String, ArrayList<IMixinInfo>> mixinsForClass : mixinsByClass.entrySet()) {
hasher.reset();
for(IMixinInfo mixin : mixinsForClass.getValue()) {
URL url = Thread.currentThread().getContextClassLoader().getResource(mixin.getClassName().replace('.', '/') + ".class");
if(url == null)
throw new IllegalStateException("Can't find " + mixin.getClassName());
byte[] bytecode;
try {
bytecode = Resources.asByteSource(url).read();
} catch(IOException e) {
throw new RuntimeException(e);
}
hasher.update(bytecode);
}
hashesByClass.put(mixinsForClass.getKey().replace('/', '.'), hasher.digest());
}
} catch(ReflectiveOperationException e) {
throw new RuntimeException(e);
}
}
}
return hashesByClass.getOrDefault(className, new byte[0]);
}
}

View File

@ -13,6 +13,7 @@ import org.spongepowered.asm.mixin.extensibility.IMixinConfigPlugin;
import org.spongepowered.asm.mixin.extensibility.IMixinInfo;
import java.io.File;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.URL;
@ -26,9 +27,6 @@ public class ModernFixMixinPlugin implements IMixinConfigPlugin {
private final Logger logger = LogManager.getLogger("ModernFix");
public static ModernFixEarlyConfig config = null;
private static final boolean USE_TRANSFORMER_CACHE = false;
private static final boolean USE_CLASS_LOCATION_CACHE = false;
public ModernFixMixinPlugin() {
try {
config = ModernFixEarlyConfig.load(new File("./config/modernfix-mixins.properties"));
@ -53,7 +51,11 @@ public class ModernFixMixinPlugin implements IMixinConfigPlugin {
TransformStore store = ObfuscationReflectionHelper.getPrivateValue(ClassTransformer.class, t, "transformers");
LaunchPluginHandler pluginHandler = ObfuscationReflectionHelper.getPrivateValue(ClassTransformer.class, t, "pluginHandler");
TransformerAuditTrail trail = ObfuscationReflectionHelper.getPrivateValue(ClassTransformer.class, t, "auditTrail");
classTransformerField.set(loader, new ModernFixCachingClassTransformer(store, pluginHandler, (TransformingClassLoader)loader, trail));
Class<?> newTransformerClass = Class.forName("cpw.mods.modlauncher.ModernFixCachingClassTransformer", true, ClassTransformer.class.getClassLoader());
Constructor<?> constructor = newTransformerClass.getConstructor(TransformStore.class, LaunchPluginHandler.class, TransformingClassLoader.class, TransformerAuditTrail.class);
ClassTransformer newTransformer = (ClassTransformer)constructor.newInstance(store, pluginHandler, loader, trail);
classTransformerField.set(loader, newTransformer);
logger.info("Successfully injected caching transformer");
}
if(isOptionEnabled("launch.class_search_cache.ModernFixResourceFinder")) {
Field resourceFinderField = TransformingClassLoader.class.getDeclaredField("resourceFinder");