diff --git a/common/src/main/java/org/embeddedt/modernfix/command/ModernFixCommands.java b/common/src/main/java/org/embeddedt/modernfix/command/ModernFixCommands.java index cafbe0af..dac52423 100644 --- a/common/src/main/java/org/embeddedt/modernfix/command/ModernFixCommands.java +++ b/common/src/main/java/org/embeddedt/modernfix/command/ModernFixCommands.java @@ -2,9 +2,34 @@ package org.embeddedt.modernfix.command; import com.mojang.brigadier.CommandDispatcher; import net.minecraft.commands.CommandSourceStack; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerLevel; +import org.embeddedt.modernfix.duck.IProfilingServerFunctionManager; + +import static net.minecraft.commands.Commands.literal; public class ModernFixCommands { public static void register(CommandDispatcher dispatcher) { + dispatcher.register(literal("modernfix") + .then(literal("mcfunctions").requires(source -> source.hasPermission(3)) + .executes(context -> { + ServerLevel level = context.getSource().getLevel(); + if(level == null) { + context.getSource().sendFailure(Component.literal("Couldn't find server level")); + return 0; + } + if (level.getServer().getFunctions() instanceof IProfilingServerFunctionManager profiler) { + context.getSource().sendSuccess(() -> Component.literal("mcfunction runtime breakdown:"), false); + for(String line : profiler.mfix$getProfilingResults().split("\n")) { + context.getSource().sendSuccess(() -> Component.literal(line), false); + } + return 1; + } else { + context.getSource().sendFailure(Component.literal("ModernFix mcfunction profiling is not enabled on this server.")); + return 0; + } + })) + ); } } diff --git a/common/src/main/java/org/embeddedt/modernfix/common/mixin/feature/mcfunction_profiling/ServerFunctionManagerMixin.java b/common/src/main/java/org/embeddedt/modernfix/common/mixin/feature/mcfunction_profiling/ServerFunctionManagerMixin.java new file mode 100644 index 00000000..58629f67 --- /dev/null +++ b/common/src/main/java/org/embeddedt/modernfix/common/mixin/feature/mcfunction_profiling/ServerFunctionManagerMixin.java @@ -0,0 +1,73 @@ +package org.embeddedt.modernfix.common.mixin.feature.mcfunction_profiling; + +import com.google.common.base.Stopwatch; +import com.llamalad7.mixinextras.sugar.Local; +import com.llamalad7.mixinextras.sugar.Share; +import com.llamalad7.mixinextras.sugar.ref.LocalRef; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.functions.CommandFunction; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.ServerFunctionManager; +import org.embeddedt.modernfix.duck.IProfilingServerFunctionManager; +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.CallbackInfo; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.Map; + +@Mixin(ServerFunctionManager.class) +public class ServerFunctionManagerMixin implements IProfilingServerFunctionManager { + @Shadow @Final private static ResourceLocation TICK_FUNCTION_TAG; + + private final Map mfix$functionWatches = new Object2ObjectOpenHashMap<>(); + + @Inject(method = "executeTagFunctions", at = @At("HEAD")) + private void resetWatches(Collection> functionObjects, ResourceLocation identifier, CallbackInfo ci) { + mfix$functionWatches.values().forEach(Stopwatch::reset); + } + + @Inject(method = "executeTagFunctions", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/ServerFunctionManager;execute(Lnet/minecraft/commands/functions/CommandFunction;Lnet/minecraft/commands/CommandSourceStack;)V")) + private void startWatch(Collection> functionObjects, ResourceLocation identifier, CallbackInfo ci, @Local(ordinal = 0) CommandFunction function, @Share("stopwatch") LocalRef watchRef) { + watchRef.set(null); + if (identifier == TICK_FUNCTION_TAG) { + var watch = mfix$functionWatches.computeIfAbsent(function.id(), i -> Stopwatch.createUnstarted()); + watch.start(); + watchRef.set(watch); + } + } + + @Inject(method = "executeTagFunctions", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/ServerFunctionManager;execute(Lnet/minecraft/commands/functions/CommandFunction;Lnet/minecraft/commands/CommandSourceStack;)V", shift = At.Shift.AFTER)) + private void stopWatch(Collection> functionObjects, ResourceLocation identifier, CallbackInfo ci, @Share("stopwatch") LocalRef watchRef) { + var watch = watchRef.get(); + if (watch != null && watch.isRunning()) { + watch.stop(); + } + } + + @Inject(method = "executeTagFunctions", at = @At("RETURN")) + private void pruneUnusedWatches(Collection> functionObjects, ResourceLocation identifier, CallbackInfo ci) { + mfix$functionWatches.values().removeIf(watch -> watch.elapsed().isZero()); + } + + @Override + public String mfix$getProfilingResults() { + var list = new ArrayList<>(mfix$functionWatches.entrySet()); + list.sort(Comparator., Duration>comparing(e -> e.getValue().elapsed()).reversed()); + StringBuilder sb = new StringBuilder(); + for (var entry : list) { + sb.append(entry.getKey().toString()); + sb.append(" - "); + sb.append(entry.getValue().toString()); + sb.append('\n'); + } + return sb.toString(); + } +} diff --git a/common/src/main/java/org/embeddedt/modernfix/common/mixin/feature/measure_time/ProfiledReloadInstanceMixin.java b/common/src/main/java/org/embeddedt/modernfix/common/mixin/feature/measure_time/ProfiledReloadInstanceMixin.java index 349c082e..33f4d60f 100644 --- a/common/src/main/java/org/embeddedt/modernfix/common/mixin/feature/measure_time/ProfiledReloadInstanceMixin.java +++ b/common/src/main/java/org/embeddedt/modernfix/common/mixin/feature/measure_time/ProfiledReloadInstanceMixin.java @@ -8,16 +8,38 @@ import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.ModifyVariable; import java.util.ArrayList; +import java.util.Comparator; import java.util.List; @Mixin(ProfiledReloadInstance.class) public class ProfiledReloadInstanceMixin { + /** + * @author embeddedt + * @reason Decorate reload listeners with their class name as well as the simple name + */ @ModifyVariable(method = "", at = @At("HEAD"), argsOnly = true, ordinal = 0) private static List getWrappedListeners(List listeners) { List newList = new ArrayList<>(listeners.size()); for(PreparableReloadListener listener : listeners) { - newList.add(new NamedPreparableResourceListener(listener)); + // No need to wrap listeners that are already wrapped/provided by a mod loader + String className = listener.getClass().getName(); + if (className.startsWith("net.minecraftforge.") || className.startsWith("net.neoforged.") || className.startsWith("net.fabricmc.")) { + newList.add(listener); + } else { + newList.add(new NamedPreparableResourceListener(listener)); + } } return newList; } + + /** + * @author embeddedt + * @reason Place most expensive reload listeners first + */ + @ModifyVariable(method = "finish", ordinal = 0, argsOnly = true, at = @At("HEAD")) + private List sortStates(List datapoints) { + datapoints = new ArrayList<>(datapoints); + datapoints.sort(Comparator.comparingLong(s -> s.preparationNanos.get() + s.reloadNanos.get()).reversed()); + return datapoints; + } } diff --git a/common/src/main/java/org/embeddedt/modernfix/common/mixin/perf/resourcepacks/PathPackResourcesMixin.java b/common/src/main/java/org/embeddedt/modernfix/common/mixin/perf/resourcepacks/PathPackResourcesMixin.java new file mode 100644 index 00000000..6b9d9d01 --- /dev/null +++ b/common/src/main/java/org/embeddedt/modernfix/common/mixin/perf/resourcepacks/PathPackResourcesMixin.java @@ -0,0 +1,82 @@ +package org.embeddedt.modernfix.common.mixin.perf.resourcepacks; + +import net.minecraft.server.packs.PackLocationInfo; +import net.minecraft.server.packs.PackResources; +import net.minecraft.server.packs.PackType; +import net.minecraft.server.packs.PathPackResources; +import org.embeddedt.modernfix.resources.ICachingResourcePack; +import org.embeddedt.modernfix.resources.PackResourcesCacheEngine; +import org.embeddedt.modernfix.util.PackTypeHelper; +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.Redirect; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.util.Objects; +import java.util.Set; + +@Mixin(value = PathPackResources.class, priority = 1100) +public abstract class PathPackResourcesMixin implements ICachingResourcePack { + + @Shadow @Final private Path root; + private PackResourcesCacheEngine cacheEngine; + + @Inject(method = "", at = @At("TAIL")) + private void cacheResources(PackLocationInfo location, Path root, CallbackInfo ci) { + invalidateCache(); + } + + private PackResourcesCacheEngine generateResourceCache() { + synchronized (this) { + PackResourcesCacheEngine engine = this.cacheEngine; + if(engine != null) + return engine; + this.cacheEngine = engine = new PackResourcesCacheEngine((type) -> this.root.resolve(type.getDirectory())); + return engine; + } + } + + @Override + public void invalidateCache() { + this.cacheEngine = null; + } + + @Inject(method = "getNamespaces", at = @At("HEAD"), cancellable = true) + private void useCacheForNamespaces(PackType type, CallbackInfoReturnable> cir) { + PackResourcesCacheEngine engine = cacheEngine; + if(engine != null) { + Set namespaces = engine.getNamespaces(type); + if(namespaces != null) + cir.setReturnValue(namespaces); + } + } + + @Redirect(method = "getRootResource", at = @At(value = "INVOKE", target = "Ljava/nio/file/Files;exists(Ljava/nio/file/Path;[Ljava/nio/file/LinkOption;)Z")) + private boolean useCacheForExistence(Path path, LinkOption[] options, String[] originalPaths) { + // the cache only stores things with a namespace and pack type + if(originalPaths.length < 3 || (!Objects.equals(originalPaths[0], "assets") && !Objects.equals(originalPaths[0], "data"))) + return Files.exists(path, options); + else + return this.generateResourceCache().hasResource(originalPaths); + } + + /** + * @author embeddedt + * @reason Use cached listing of mod resources + */ + @Inject(method = "listResources", at = @At("HEAD"), cancellable = true) + private void fastGetResources(PackType type, String namespace, String path, PackResources.ResourceOutput resourceOutput, CallbackInfo ci) + { + if(!PackTypeHelper.isVanillaPackType(type)) + return; + ci.cancel(); + this.generateResourceCache().collectResources(type, namespace, path.split("/"), Integer.MAX_VALUE, resourceOutput); + } +} diff --git a/common/src/main/java/org/embeddedt/modernfix/common/mixin/perf/resourcepacks/ReloadableResourceManagerMixin.java b/common/src/main/java/org/embeddedt/modernfix/common/mixin/perf/resourcepacks/ReloadableResourceManagerMixin.java deleted file mode 100644 index 3fd155cf..00000000 --- a/common/src/main/java/org/embeddedt/modernfix/common/mixin/perf/resourcepacks/ReloadableResourceManagerMixin.java +++ /dev/null @@ -1,18 +0,0 @@ -package org.embeddedt.modernfix.common.mixin.perf.resourcepacks; - -import net.minecraft.server.packs.resources.ReloadableResourceManager; -import org.embeddedt.modernfix.ModernFix; -import org.embeddedt.modernfix.resources.PackResourcesCacheEngine; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; - -@Mixin(ReloadableResourceManager.class) -public class ReloadableResourceManagerMixin { - @Inject(method = "createReload", at = @At("HEAD")) - private void invalidateResourceCaches(CallbackInfoReturnable cir) { - ModernFix.LOGGER.info("Invalidating pack caches"); - PackResourcesCacheEngine.invalidate(); - } -} diff --git a/common/src/main/java/org/embeddedt/modernfix/duck/IProfilingServerFunctionManager.java b/common/src/main/java/org/embeddedt/modernfix/duck/IProfilingServerFunctionManager.java new file mode 100644 index 00000000..f1337e6f --- /dev/null +++ b/common/src/main/java/org/embeddedt/modernfix/duck/IProfilingServerFunctionManager.java @@ -0,0 +1,5 @@ +package org.embeddedt.modernfix.duck; + +public interface IProfilingServerFunctionManager { + String mfix$getProfilingResults(); +} diff --git a/common/src/main/java/org/embeddedt/modernfix/entity/EntityRendererMap.java b/common/src/main/java/org/embeddedt/modernfix/entity/EntityRendererMap.java index f9532eaf..b2ab9510 100644 --- a/common/src/main/java/org/embeddedt/modernfix/entity/EntityRendererMap.java +++ b/common/src/main/java/org/embeddedt/modernfix/entity/EntityRendererMap.java @@ -3,6 +3,8 @@ package org.embeddedt.modernfix.entity; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; +import com.google.common.collect.Iterators; +import com.google.common.collect.Maps; import net.minecraft.client.renderer.entity.EntityRenderer; import net.minecraft.client.renderer.entity.EntityRendererProvider; import net.minecraft.client.renderer.entity.EntityRenderers; @@ -12,14 +14,20 @@ import org.embeddedt.modernfix.ModernFix; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.util.AbstractCollection; +import java.util.AbstractSet; import java.util.Collection; +import java.util.Iterator; import java.util.Map; +import java.util.Objects; +import java.util.Optional; import java.util.Set; import java.util.concurrent.ExecutionException; +@SuppressWarnings("OptionalAssignedToNull") public class EntityRendererMap implements Map, EntityRenderer> { private final Map, EntityRendererProvider> rendererProviders; - private final LoadingCache, EntityRenderer> rendererMap; + private final LoadingCache, Optional>> rendererMap; private final EntityRendererProvider.Context context; public EntityRendererMap(Map, EntityRendererProvider> rendererProviders, EntityRendererProvider.Context context) { @@ -28,22 +36,22 @@ public class EntityRendererMap implements Map, EntityRenderer> this.rendererMap = CacheBuilder.newBuilder().build(new RenderConstructor()); } - class RenderConstructor extends CacheLoader, EntityRenderer> { + class RenderConstructor extends CacheLoader, Optional>> { @Override - public EntityRenderer load(EntityType key) throws Exception { + public Optional> load(EntityType key) throws Exception { EntityRendererProvider provider = rendererProviders.get(key); + if(provider == null) + return Optional.empty(); synchronized(EntityRenderers.class) { EntityRenderer renderer; try { - if(provider == null) - throw new RuntimeException("Provider not registered"); renderer = provider.create(context); ModernFix.LOGGER.info("Loaded entity {}", BuiltInRegistries.ENTITY_TYPE.getKey(key)); } catch(RuntimeException e) { ModernFix.LOGGER.error("Failed to create entity model for " + BuiltInRegistries.ENTITY_TYPE.getKey(key) + ":", e); renderer = new ErroredEntityRenderer<>(context); } - return renderer; + return Optional.ofNullable(renderer); } } } @@ -71,10 +79,8 @@ public class EntityRendererMap implements Map, EntityRenderer> @Override public EntityRenderer get(Object o) { try { - EntityRenderer renderer = rendererMap.get((EntityType)o); - if(renderer == null) - throw new AssertionError("Returned entity renderer should never be null"); - return renderer; + Optional> renderer = rendererMap.get((EntityType)o); + return renderer.orElse(null); } catch (IllegalStateException e) { return null; /* emulate value not being present if recursive load occurs */ } catch (ExecutionException e) { @@ -85,21 +91,21 @@ public class EntityRendererMap implements Map, EntityRenderer> @Nullable @Override public EntityRenderer put(EntityType entityType, EntityRenderer entityRenderer) { - EntityRenderer old = rendererMap.getIfPresent(entityType); - rendererMap.put(entityType, entityRenderer); - return old; + Optional> old = rendererMap.getIfPresent(entityType); + rendererMap.put(entityType, Optional.ofNullable(entityRenderer)); + return old != null ? old.orElse(null) : null; } @Override public EntityRenderer remove(Object o) { - EntityRenderer r = rendererMap.getIfPresent(o); + Optional> old = rendererMap.getIfPresent(o); rendererMap.invalidate(o); - return r; + return old != null ? old.orElse(null) : null; } @Override public void putAll(@NotNull Map, ? extends EntityRenderer> map) { - rendererMap.putAll(map); + rendererMap.putAll(Maps.transformValues(map, Optional::ofNullable)); } @Override @@ -116,12 +122,64 @@ public class EntityRendererMap implements Map, EntityRenderer> @NotNull @Override public Collection> values() { - return rendererMap.asMap().values(); + return new AbstractCollection<>() { + @Override + public Iterator> iterator() { + return Iterators.transform(Iterators.unmodifiableIterator(rendererProviders.keySet().iterator()), EntityRendererMap.this::get); + } + + @Override + public int size() { + return rendererProviders.size(); + } + }; + } + + private class Entry implements Map.Entry, EntityRenderer> { + private final EntityType key; + + private Entry(EntityType key) { + this.key = key; + } + + @Override + public EntityType getKey() { + return key; + } + + @Override + public EntityRenderer getValue() { + return get(key); + } + + @Override + public EntityRenderer setValue(EntityRenderer value) { + return put(key, value); + } } @NotNull @Override public Set, EntityRenderer>> entrySet() { - return rendererMap.asMap().entrySet(); + return new AbstractSet<>() { + @Override + public Iterator, EntityRenderer>> iterator() { + return Iterators.transform(Iterators.unmodifiableIterator(rendererProviders.keySet().iterator()), Entry::new); + } + + @Override + public boolean contains(Object o) { + if (o instanceof Map.Entry e) { + return rendererProviders.containsKey(e.getKey()) && Objects.equals(get(e.getKey()), e.getValue()); + } else { + return false; + } + } + + @Override + public int size() { + return rendererProviders.size(); + } + }; } } diff --git a/common/src/main/java/org/embeddedt/modernfix/resources/PackResourcesCacheEngine.java b/common/src/main/java/org/embeddedt/modernfix/resources/PackResourcesCacheEngine.java index 2c347e36..41be7011 100644 --- a/common/src/main/java/org/embeddedt/modernfix/resources/PackResourcesCacheEngine.java +++ b/common/src/main/java/org/embeddedt/modernfix/resources/PackResourcesCacheEngine.java @@ -3,88 +3,131 @@ package org.embeddedt.modernfix.resources; import com.google.common.base.Joiner; import com.google.common.base.Stopwatch; import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; -import com.mojang.datafixers.util.Pair; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.packs.PackResources; import net.minecraft.server.packs.PackType; import org.embeddedt.modernfix.ModernFix; -import org.embeddedt.modernfix.platform.ModernFixPlatformHooks; import org.embeddedt.modernfix.util.PackTypeHelper; +import org.jetbrains.annotations.Nullable; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.*; -import java.util.function.BiFunction; import java.util.function.Function; -import java.util.function.Predicate; -import java.util.stream.Collectors; import java.util.stream.Stream; /** * The core of the resource pack cache system. - * - * Using a dedicated set and also separate lists is important; testing without this showed a huge performance - * drop. */ public class PackResourcesCacheEngine { private static final Joiner SLASH_JOINER = Joiner.on('/'); - private final Map> namespacesByType; - private final Set containedPaths; - private final EnumMap>> resourceListings; + static class Node { + Map children; + + void optimize() { + if (children != null) { + for (var entry : children.entrySet()) { + var oldNode = entry.getValue(); + oldNode.optimize(); + if (oldNode.children == null) { + entry.setValue(EMPTY); + } + } + children = Map.copyOf(children); + } else { + children = Map.of(); + } + } + + void collectResources(String namespace, Path baseNioPath, String[] pathComponents, int curIndex, int maxDepth, PackResources.ResourceOutput output) { + if (curIndex > maxDepth) { + return; + } + if (curIndex < pathComponents.length) { + String component; + do { + component = pathComponents[curIndex]; + if (!component.isEmpty()) { + break; + } + curIndex++; + maxDepth++; + } while(true); + + Node n = getChild(component); + if (n != null) { + n.collectResources(namespace, baseNioPath, pathComponents, curIndex + 1, maxDepth, output); + } + } else { + // We reached the desired path. Collect all resources + this.outputResources(namespace, baseNioPath, String.join("/", pathComponents), output); + } + + } + + void outputResources(String namespace, Path baseNioPath, String path, PackResources.ResourceOutput output) { + if (children.isEmpty()) { + // This is a terminal node. + ResourceLocation location = ResourceLocation.fromNamespaceAndPath(namespace, path); + output.accept(location, () -> Files.newInputStream(baseNioPath.resolve(path))); + } else { + for (var entry : children.entrySet()) { + entry.getValue().outputResources(namespace, baseNioPath, path + "/" + entry.getKey(), output); + } + } + } + + @Nullable + Node getChild(String name) { + return children.get(name); + } + } + + private static final Node EMPTY = new Node(); + + private final Node root = new Node(); + private final Map rootPathsByType = new Object2ObjectOpenHashMap<>(); + private volatile boolean cacheGenerationFlag = false; private List cacheGenerationTasks = new ArrayList<>(); private Path debugPath; - public PackResourcesCacheEngine(Function> namespacesRetriever, BiFunction basePathRetriever) { - this.namespacesByType = new EnumMap<>(PackType.class); - for(PackType type : PackType.values()) { - if(!PackTypeHelper.isVanillaPackType(type)) - continue; - this.namespacesByType.put(type, namespacesRetriever.apply(type)); - } - this.containedPaths = new ObjectOpenHashSet<>(); - this.resourceListings = new EnumMap<>(PackType.class); + public PackResourcesCacheEngine(Function basePathRetriever) { // used for log message - this.debugPath = basePathRetriever.apply(PackType.CLIENT_RESOURCES, "minecraft").toAbsolutePath(); + this.debugPath = basePathRetriever.apply(PackType.CLIENT_RESOURCES).toAbsolutePath(); + this.root.children = new Object2ObjectOpenHashMap<>(); + ObjectOpenHashSet pathKeys = new ObjectOpenHashSet<>(); for(PackType type : PackType.values()) { - Collection namespaces = PackTypeHelper.isVanillaPackType(type) ? this.namespacesByType.get(type) : namespacesRetriever.apply(type); - Collection> namespacedRoots = namespaces.stream().map(s -> Pair.of(s, basePathRetriever.apply(type, s).toAbsolutePath())).collect(Collectors.toList()); + var typeRoot = new Node(); + this.root.children.put(type.getDirectory(), typeRoot); + Path root = basePathRetriever.apply(type); + this.rootPathsByType.put(type, root); cacheGenerationTasks.add(() -> { - ImmutableMap.Builder> packTypedMap = ImmutableMap.builder(); - for(Pair pair : namespacedRoots) { - try { - ImmutableList.Builder namespacedList = ImmutableList.builder(); - String namespace = pair.getFirst(); - Path root = pair.getSecond(); - String[] prefix = new String[] { type.getDirectory(), namespace }; - try (Stream stream = Files.find(root, Integer.MAX_VALUE, (p, a) -> a.isRegularFile())) { - stream - .map(path -> root.relativize(path.toAbsolutePath())) - .filter(PackResourcesCacheEngine::isValidCachedResourcePath) - .forEach(path -> { - CachedResourcePath cachedPath = new CachedResourcePath(prefix, path); - synchronized (this.containedPaths) { - this.containedPaths.add(cachedPath); + try { + try (Stream stream = Files.find(root, Integer.MAX_VALUE, (p, a) -> a.isRegularFile())) { + stream + .map(path -> root.relativize(path.toAbsolutePath())) + .filter(PackResourcesCacheEngine::isValidCachedResourcePath) + .forEach(path -> { + var node = typeRoot; + for (Path component : path) { + String key = pathKeys.addOrGet(component.toString()); + if (node.children == null) { + node.children = new Object2ObjectOpenHashMap<>(); } - //if(!cachedPath.getFileName().endsWith(".mcmeta")) - namespacedList.add(cachedPath); - }); - } - packTypedMap.put(namespace, namespacedList.build()); - } catch(IOException ignored) { + node = node.children.computeIfAbsent(key, $ -> new Node()); + } + }); } - } - synchronized (this.resourceListings) { - this.resourceListings.put(type, packTypedMap.build()); + } catch(IOException ignored) { } }); } - cacheGenerationTasks.add(() -> { - ((ObjectOpenHashSet)this.containedPaths).trim(); - }); + cacheGenerationTasks.add(this.root::optimize); } private static boolean isValidCachedResourcePath(Path path) { @@ -103,8 +146,9 @@ public class PackResourcesCacheEngine { } public Set getNamespaces(PackType type) { + awaitLoad(); if(PackTypeHelper.isVanillaPackType(type)) - return this.namespacesByType.get(type); + return this.root.getChild(type.getDirectory()).children.keySet(); else return null; } @@ -131,56 +175,34 @@ public class PackResourcesCacheEngine { } } - public boolean hasResource(String path) { - awaitLoad(); - return this.containedPaths.contains(new CachedResourcePath(path)); - } - public boolean hasResource(String[] paths) { awaitLoad(); - return this.containedPaths.contains(new CachedResourcePath(paths)); + var node = this.root; + for (String path : paths) { + if (path.isEmpty()) { + continue; + } + node = node.children.get(path); + if (node == null) { + //ModernFix.LOGGER.info("Does not have " + String.join("/", paths)); + return false; + } + } + return true; } - public Collection getResources(PackType type, String resourceNamespace, String pathIn, int maxDepth, Predicate filter) { + public void collectResources(PackType type, String resourceNamespace, String[] components, int maxDepth, PackResources.ResourceOutput output) { if(!PackTypeHelper.isVanillaPackType(type)) throw new IllegalArgumentException("Only vanilla PackTypes are supported"); awaitLoad(); - List paths = resourceListings.get(type).getOrDefault(resourceNamespace, Collections.emptyList()); - if(paths.isEmpty()) - return Collections.emptyList(); - String testPath = pathIn.endsWith("/") ? pathIn : (pathIn + "/"); - ArrayList resources = new ArrayList<>(); - for(CachedResourcePath cachePath : paths) { - if((cachePath.getNameCount() - 2) > maxDepth) - continue; - String fullPath = cachePath.getFullPath(2); - String fullTestPath = fullPath.endsWith("/") ? fullPath : (fullPath + "/"); - if(!fullTestPath.startsWith(testPath)) { - continue; - } - ResourceLocation foundResource = ResourceLocation.fromNamespaceAndPath(resourceNamespace, fullPath); - if(!filter.test(foundResource)) - continue; - resources.add(foundResource); - } - return resources; - } - - private static final WeakHashMap cachingPacks = new WeakHashMap<>(); - public static void track(ICachingResourcePack pack) { - synchronized (cachingPacks) { - cachingPacks.put(pack, Boolean.TRUE); - } - } - - public static void invalidate() { - if(!ModernFixPlatformHooks.INSTANCE.isDevEnv()) + var node = this.root.getChild(type.getDirectory()); + if (node == null) { return; - synchronized (cachingPacks) { - cachingPacks.keySet().forEach(pack -> { - if(pack != null) - pack.invalidateCache(); - }); } + node = node.getChild(resourceNamespace); + if (node == null) { + return; + } + node.collectResources(resourceNamespace, this.rootPathsByType.get(type).resolve(resourceNamespace), components, 0, maxDepth, output); } } diff --git a/common/src/main/resources/modernfix.accesswidener b/common/src/main/resources/modernfix.accesswidener index d8ac5f7a..6ec128cf 100644 --- a/common/src/main/resources/modernfix.accesswidener +++ b/common/src/main/resources/modernfix.accesswidener @@ -56,5 +56,8 @@ accessible field net/minecraft/client/renderer/entity/EnderDragonRenderer$Dragon accessible method net/minecraft/world/level/block/state/StateDefinition appendPropertyCodec (Lcom/mojang/serialization/MapCodec;Ljava/util/function/Supplier;Ljava/lang/String;Lnet/minecraft/world/level/block/state/properties/Property;)Lcom/mojang/serialization/MapCodec; accessible class net/minecraft/client/multiplayer/SessionSearchTrees$Key +accessible field net/minecraft/server/packs/resources/ProfiledReloadInstance$State preparationNanos Ljava/util/concurrent/atomic/AtomicLong; +accessible field net/minecraft/server/packs/resources/ProfiledReloadInstance$State reloadNanos Ljava/util/concurrent/atomic/AtomicLong; + accessible class net/minecraft/world/item/crafting/Ingredient$Value accessible class net/minecraft/world/item/crafting/Ingredient$ItemValue diff --git a/neoforge/src/main/java/org/embeddedt/modernfix/neoforge/mixin/feature/measure_time/AddReloadListenerEventWrapperMixin.java b/neoforge/src/main/java/org/embeddedt/modernfix/neoforge/mixin/feature/measure_time/AddReloadListenerEventWrapperMixin.java new file mode 100644 index 00000000..9dd439b8 --- /dev/null +++ b/neoforge/src/main/java/org/embeddedt/modernfix/neoforge/mixin/feature/measure_time/AddReloadListenerEventWrapperMixin.java @@ -0,0 +1,20 @@ +package org.embeddedt.modernfix.neoforge.mixin.feature.measure_time; + +import net.minecraft.server.packs.resources.PreparableReloadListener; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +@Mixin(targets = "net/neoforged/neoforge/event/AddReloadListenerEvent$WrappedStateAwareListener") +public abstract class AddReloadListenerEventWrapperMixin implements PreparableReloadListener { + @Shadow @Final private PreparableReloadListener wrapped; + + /** + * @author embeddedt + * @reason make a proper name show up in ProfiledReloadInstance + */ + @Override + public String getName() { + return this.wrapped.getClass().getName(); + } +}