Collections Of Optimizations

Quick rating

Collections Of Optimizations

No reviews yet

Mod-targeted optimizations that reduce memory usage and stutters, improve server/client performance, and fix bugs and crashes.

Bug Fixes
Performance & Optimization
Mod Loaders
Forge
Minecraft

Community voices

Reviews

Versions
Loading versions…
Match includes

Click once to include, again to exclude, again to clear

Rating Any
Any 0.5 1.0 1.5 2.0 2.5 3.0 3.5 4.0 4.5 5.0
Min
Max
Play Status
Reviews
Time Played
hrs+
Verified developers only
Has developer response
List view
Grid view
Compact view
Sort by
Date
Rating
Helpful
Unhelpful
Edited
Sort ascending
Delete this review?

This removes your review from the project. You can write a new review after.

Review submitted for moderation

Your review has been sent to moderators, who will check that it meets our guidelines before it appears publicly.

No reviews yet. Be the first to review this project!

Get it on

Available Platforms

About

Project Details

Type
Mod
Latest Version
collections_of_optimizations-0.9-all.jar
Authors

For authors

Embed Badge

If you're the author of this project, you can embed a live badge anywhere that supports HTML or Markdown. It updates automatically whenever ratings change.

Custom banner text
ModDex rating badge preview

Use HTML for any page that supports it, or Markdown for README files and Markdown-based descriptions.

Identifiers

Platform IDs

CurseForge ID

Resources

External Links

Source Issues Wiki Discord

About

Description

Collections Of Optimizations

A optimization mod that mainly targets other mods instead of minecraft. Fixes slow code & bugs in other mods. Everything is properly tested so nothing should break.

Expect reduced memory usage and stutters, faster chunk loading, and improved server and client performance with less bugs and crashes.

Patreon

Compatibility

Patches only load if the mod they target is installed. There's a mixin config plugin that checks the loading mod list, so having none of these mods installed does nothing at all. Not even load. Absolutely 0 impact.

Tested against 507 mods.


Which mods are touched:

  • Structurify
  • Block Swap
  • Blood Magic
  • Animus
  • Patchouli
  • Just Dire Things
  • Goety's Delight
  • TerraBlender
  • FTB Chunks
  • Curios API
  • Terramity
  • Armageddon
  • Born In Chaos
  • Bosses Rise
  • Marium's Soulslike Weaponry
  • Kind Of Nice Weapon
  • Punchy
  • L2 Hostility
  • Immersive Aircraft
  • Artifacts
  • Oculus
  • Nature's Aura
  • Echelon - Linggango Variant
  • Integrated API
  • Elysium API
  • Balm
  • Caelus API
  • Lootr
  • Xaero's Minimap
  • Xaero's + Waystones Compatibility
  • GeckoLib
  • Macabre (not released, still being worked on)
  • Alex's Mobs (not released, quite done)
  • Alex's Caves (done, testing)
  • Ad Astra (done, tested gonna publish sum time)
  • Vanilla Minecraft (Item Rendering)

Structurify

The problem is four of it's hooks sit on the chunk generation path and not a single one of them was written like it knew that. Vanilla runs createStructures once for every chunk it generates, and inside that it loops over every structure set in the registry. On a big pack that's a few hundred. So anything Structurify does "per structure set" is really "per structure set, per chunk".

Four things fixed here:

  • Every spacing, separation, salt etc etc, override funnels through one helper that does containsKey and then get on a TreeMap keyed by structure set id. Two tree descents of full resource location string compares to fetch one value. And it fires a lot (hotpath): getPotentialStructureChunk reads the spacing field like five separate times, Structurify's field hook has no ordinal on it so it fires on all five, separation calls the already hooked spacing() on top of that, plus salt. Eight resolutions, sixteen tree descents, per structure set, per chunk. On a few hundred sets that's tens of thousands of string compares for every chunk. Now it's one hash lookup, and since all eight calls use the same String instance in a row, seven of them get answered by a reference compare.

  • checkStructure is four nested computeIfAbsent calls on four ConcurrentHashMaps bolted onto the ChunkGenerator. Every one boxes a mixed 64 bit id into a Long, and every lambda captures stuff, the outer one captures nine values, so the lambda object gets allocated before the call even runs. You pay for that on cache hits too, and the call happens once per structure set entry per chunk load. On a miss it also writes four entries that nothing ever removes. And all three checks it drives are off by default, so on a stock config every bit of that exists to produce a value that is always true. Now it returns true at the top and touches nothing. It only takes that shortcut while Structurify's own result map is still empty, so it can't ever disagree with something already cached in there. Heads up though: this one goes inert if you turn on prevent_structure_overlap or the global flatness check, because then the checks genuinely have work to do and there's no way inside their lambdas.

  • The terrain height cache is a thread local LinkedHashMap with access ordering on. Per query that's a key object for the get, a second key object for the put, an Integer box for the value, another for the return, a map entry, and a relink of the access order list. All replacing one virtual call. Worse, the key holds the ChunkGenerator, the RandomState and the LevelHeightAccessor, which IS the ServerLevel, up to 16384 of them per worldgen thread. So leaving a dimension doesn't let it go, it just sits there. Now it's one Long2IntOpenHashMap per thread keyed on position and heightmap type only, with the identity guard held weakly so nothing gets pinned. Zero allocations per query, and the answer can't change because getBaseHeight is a pure function of its inputs.

  • The overlap check builds a LongOpenHashSet but declares the variable as Set<Long>, so every add boxes, then it copies out through Iterator<Long> and boxes all over again. Two Long allocations per section of every piece. Only bites you with prevent_structure_overlap on but it was free to fix.

Tested on 2.0.29.


Block Swap

Block Swap hooks ChunkHolder.broadcastChanges, so the first time a chunk gets broadcast it does a full sweep of every block in it. That's up to 98,304 iterations per chunk, each one allocating a BlockPos and calling Level.getBlockState, which re-resolves the chunk and section for a block whose section is already sitting right there in a local variable.

This filters each section through its block state palette first. If the palette doesn't contain any block you configured, the section can't contain one either, so the 4096 block walk gets skipped. Reads come straight from the section now, and BlockPos only gets allocated when something actually swaps.

Was 10.1% of my server tick. Fattest thing in the whole profile.


Blood Magic

Three things fixed:

  • The item routing network. isConnected is a depth first search from a node back to the master, and the path argument it walks with is a pure visited set, it only ever gets add and contains called on it and never backtracks. Blood Magic hands it a LinkedList, so every single contains is a linear scan over everything visited so far. That turns one search from O(V+E) into O(V*E). Then tick runs that search once for every output node AND once for every input node, so the cost of one routing cycle goes up with roughly the CUBE of how big your network is. And the cycle period is max(1, 20 - itemCount), so if you've got a stack of 19 or more in the master node's acceleration slot it runs the whole thing every single tick. Now the visited set is an actual set. Same nodes visited in the same order, same answer, because membership is the only thing ever read off that list.

  • The ARC rebuilding its recipe list every tick. getARC gets called straight out of TileAlchemicalReactionChamber tick, and the ticker Blood Magic registers has no side check on it, so that's client AND server, every tick, for every ARC you own. Any time the input and tool slots are both filled, which is just "the machine is doing its job", it calls getAllRecipesFor, and in 1.20.1 that's List.copyOf(byType(type).values()). A brand new immutable list holding every ARC recipe in the pack, twenty times a second per machine, to be walked once and thrown in the bin. Cached now. Same list, same order, rebuilt when recipes reload.

  • The ARC's furnace mode. It calls getRecipeFor(SMELTING, ...) from the tick too, on both sides, every tick. That walks every smelting recipe in the pack calling matches() until something hits, and with KubeJS scripts and a few hundred mods that's thousands of them. Vanilla furnaces don't do this, AbstractFurnaceBlockEntity holds a RecipeManager.CachedCheck specifically so the hot path retries last tick's recipe before scanning everything. Now the ARC gets the same thing, built with vanilla's own createCheck. This one is NOT strictly identical, in the exact same narrow way vanilla furnaces aren't: if two smelting recipes both match one input it can keep handing back the one it matched last, where a fresh scan might pick the other. Recipe map order is arbitrary either way so neither is more right, and it's already how every furnace in the game behaves.

Both ARC caches invalidate on a shared generation counter that bumps on the reload listener pass and on datapack sync. Had to do it that way because RecipeManager.replaceRecipes mutates the existing manager in place, so checking the manager reference alone would happily keep serving you pre reload recipes.


Animus

The one real problem is on the render thread.

The equivalency sigil draws an outline over every matching block around whatever you're looking at, and it rebuilds that outline from scratch every single frame. SigilEquivalencyRenderer#onRenderLevelStage is a RenderLevelStageEvent handler so it runs once per frame, and any time you're holding the sigil and looking at a matching block it calls findMatchingBlocksInRadius. That method allocates an ArrayList, a HashSet, a LinkedList and four BlockPos offset objects, then breadth first searches up to (radius * 2 + 1) ^ 2 positions doing a getBlockState on each one and shoving a BlockPos into the visited set per position.

At 144 fps with the default radius that's tens of thousands of block state reads a second, plus a BlockPos allocated for every single one of them, to redraw an outline that only changes when you look somewhere else.

Now the fill gets reused while the look target, block, radius and clicked face are all unchanged. Not strictly identical but it's bounded: it still recomputes at least once per game tick, so breaking or placing a block inside the region updates the outline within one tick instead of within one frame. You won't see the difference, it's 50ms worst case.


Alex's Mobs

All three fixes are in the roughly fifty lines where it reaches out and touches vanilla mobs. ServerEvents#onEntityFinalizeSpawn puts the goals onto vanilla entities as they spawn, and Mob#serverAiStep evaluates canUse() on every other tick, so these cost ten goal evaluations a second per mob for the rest of that entity's life. They scale with how many vanilla mobs you've got loaded, not with anything Alex's Mobs spawns.

  • Every vanilla creeper gets TWO AvoidEntityGoals put into it, one for snow leopards and one for tigers. AvoidEntityGoal#canUse has no interval on it whatsoever, it runs a getEntitiesOfClass over inflate(6, 3, 6) plus a getNearestEntity every single time it's evaluated. That's whole ass 20 entity queries a second per creeper, forever, whether or not a snow leopard or a tiger exists anywhere in your world. And unlike the spider, wolf, polar bear, cat, rabbit and dolphin branches sitting right next to it, this one is behind no config option at all, so there's no way to turn it off in Alex's Mobs itself. They also get added to targetSelector instead of goalSelector, which is the wrong selector for an avoidance goal. Now the handler just cancels at the top for creepers, which is exactly the same thing as skipping those two addGoal calls, since a Creeper isn't a WanderingTrader and the chain below it is else if, so those two goals are the only thing that method does for a creeper anyway. Not identical: creepers stop running away from snow leopards and tigers.

  • Every vanilla spider gets new NearestAttackableTargetGoal<>(spider, EntityFly.class, 1, true, false, null). That third argument is the random interval, and canUse reads if (randomInterval > 0 && random.nextInt(randomInterval) != 0) return false. nextInt(1) is always 0, so the throttle never fires, not once, ever. The goal then runs a full getEntitiesOfClass(EntityFly.class, ...) over boundingBox.inflate(followRange, 4, followRange), a 33x12x33 box on a spider, ten times a second, allocating a list every time, usually to find absolutely nothing. Every other interval passed in that same method is 10, 15, 45 or 70, so the 1 looks like a typo. It's configurable now and defaults to 10, which is what wolves already get for moose. It's matched on the argument value rather than an ordinal, so it only touches the goal that was built with a 1 and it'll stop applying by itself if that ever gets corrected upstream. Not identical: spiders notice flies a bit later, set it to 1 for stock. Alex's Mobs also has its own spidersAttackFlies option if you'd rather just delete the goal outright.

  • A memory one. AMWorldData.dataMap is a HashMap<Level, AMWorldData> and BEACHED_CACHALOT_WHALE_SPAWNER_MAP is a HashMap<ServerLevel, ...> whose values also hold their level in a field. Nothing ever removes an entry and a HashMap holds its keys strongly, so every ServerLevel your session ever created stays reachable. On a dedicated server that costs you nothing since levels live as long as the process does, but in singleplayer, leaving a world and loading a different one keeps the entire previous ServerLevel graph alive. Both get cleared on server stop now. Identical, since every entry is keyed on a level belonging to the server that's going away and both maps are just caches their owners rebuild on demand.

Alex's Caves

One thing that sits on the hottest path in the entire chunk generator.

  • Alex's Caves works out where its cave biomes go from a HEAD injection on MultiNoiseBiomeSource#getNoiseBiome, and vanilla resolves a biome for every 4x4x4 thing cell of every chunk. LevelChunkSection#fillBiomesFromNoise is a triple loop over 4 by 4 by 4 shit per section, so on a 384 tall world that's 4 by 4 horizontally and 96 vertically, 1536 fucking calls per chunk. And ACBiomeRarity#getRareBiomeInfoForQuad takes (worldSeed, x, z) and ignores y completely. So all 1536 of those resolve to 16 distinct answers and 95 out of every 96 are recomputing something already known, where each one costs two PerlinSimplexNoise evaluations, a Voronoi cell search and three ForgeConfigSpec reads. Memoised now. It's a pure function of the seed and the two horizontal quart coordinates, so this is identical: the seed is part of the cache state so a different world drops it, and the two private statics it also leans on get assigned only in ACBiomeRarity#init(), which bumps a generation counter here, as does any config reload. Per thread too, so worldgen threads never share an entry. This one's a chunk loading fix more than a tick fix.

  • Inside one of its biome cells that same handler calls sampler.sample(x, y, z) and then doesn't cancel, so vanilla's own body turns around and samples the identical position again. Climate.Sampler#sample builds a SinglePointContext and computes six density functions off it, which is the expensive half of a biome lookup, and it's doing it twice back to back on the same thread with the same arguments. This is the exact same duplication Elysium API has, so it's the exact same mixin: one patch, applies if either mod is installed, either config option switches it on. Identical, sample is a pure function of the sampler and the three coords.

Patchouli

ItemStackUtil#getBookFromStack has a fast path for Patchouli's own book item, and for anything else it walks every registered book in the pack comparing items. Fine on its own. The problem is who calls it:

  • BookRightClickHandler#onRenderHUD calls it once per frame on whatever is in your main hand. Not "when you're holding a book", every frame regardless. Sword, pickaxe, empty hand, doesn't matter.

  • TooltipHandler#onTooltip calls it once for EACH of your nine hotbar slots, on every frame a tooltip is on screen.

So on a pack like mine with a few dozen guide books, hovering an item in your inventory is nine walks of the whole book list per frame. That's thousands of item comparisons a second plus a values iterator allocated nine times a frame, to answer a question about an item that hasn't changed.

Now it's a per item cache. This one is properly behaviour identical and it's nice why: ItemStack.isSameItem in 1.20.1 is literally stack.is(other.getItem()), the item and nothing else, no NBT. So the result of that loop is a pure function of the stack's item and there is nothing else it could depend on. The ItemModBook fast path reads per stack NBT so I left that completely alone, the cache only covers the fallback loop. Rebuilds if the number of registered books ever changes.


Ad Astra

The repo is old as shit so everything came out of the curseforge latest 1.20.1 forge jar, not GitHub. I had to decompile yeah.

Ad Astra's LivingEntityMixin injects at HEAD of LivingEntity#travel and calls GravityApi.API.getGravity(entity), so gravity gets resolved for every living entity, every tick, on both sides. For every client side entity, and every server side entity that isn't standing next to a gravity normalizer, that falls all the way through to getGravity(ResourceKey), which is a HashMap lookup, an Optionull hop and a float division, to hand back a per dimension constant. Memoised now. It's a pure function of the dimension key and the PLANETS map behind AdAstraData#getPlanet, and that map has exactly one writer, the planets reload listener, which clears and repopulates it in one pass. The memo gets dropped at the top of that listener so the invalidation is exact rather than timed.

The temperature twin of that method is on a much worse path, and it's the reason I even made this patch. FlowingFluidMixin wraps BlockState#isSolid() inside FlowingFluid#getNewLiquid with a TemperatureApi.API.isLiveable(level, pos) test, which drops the identical lookup onto every flowing fluid calculation in the game. Vanilla water and lava included, not just Ad Astra's own fluids. Same memo covers it.

Now, the oil wells, since somebody asked and the answer surprised me. ad_astra:oil_well is placed with spacing: 20, separation: 14 into a biome tag covering every ocean, which is denser than vanilla villages at 34/8 or ocean monuments at 32/5, and that's why you see them constantly. The template is one 15x18x16 piece holding 147 ad_astra:oil blocks and nothing else, started at WORLD_SURFACE_WG so it lands right at the water surface. Placing 147 blocks costs nothing at all. That isn't the problem.

The problem is what happens after. Oil is a FlowingFluid, so a blob of it dumped at the surface of an ocean spreads, which is why it reaches the seabed, and every flowing fluid block in the world runs spread and getNewLiquid on its fluid tick. Ad Astra injected into both: a cancellable HEAD inject on spread calling getGravity(level, pos), and the isSolid wrapper calling isLiveable(level, pos). Each of those is a DimensionDataStorage lookup plus a BlockPos keyed map get, and spread allocates a CallbackInfo on top. So you get a big body of flowing fluid, and every flowing fluid block anywhere in your world pays two Ad Astra planet data lookups per fluid tick. On the overworld neither one can ever fire: gravity is a constant 1.0 so the spread cancel is unreachable, and it's always liveable so the isSolid wrapper always just delegates. Pure overhead on Earth. This patch takes the per dimension half of both out. The CallbackInfo is Mixin's own plumbing and the position lookup genuinely has to run, so those stay.

Ad Astra ships no config option for oil wells, I checked. If you want them gone it's a datapack emptying ad_astra:tags/worldgen/biome/has_structure/oil_well.json. I aint' patching that out from here, since refusing to place a structure is a worldgen change and not a performance one.


Balm

Balm's dynamic model is the thingy mods use to retexture a block model from whatever block it got built out of, Cooking for Blockheads counters being the usual example. It caches its baked models in a Map<String, BakedModel>, and the key it uses is state.toString(), built fresh inside getQuads. ModelBlockRenderer calls getQuads six times for the faces plus once more for the null side, so seven times per block, for every block in a chunk section rebuild, and again for every item model render.

And StateHolder#toString is not a cheap string. It appends Block#toString, which is "Block{" + BuiltInRegistries.BLOCK.getKey(this) + "}", so that's a registry reverse lookup plus a fresh ResourceLocation#toString that isn't cached anywhere. Then for any state carrying properties it runs getValues().entrySet().stream().map(...).collect(Collectors.joining(",")). A whole stream pipeline, a StringJoiner, and a concat per property, rebuilt from nothing on every single call, just to look up a map that already had the answer sitting in it.

Memoised on the state now. StateHolder overrides neither equals nor hashCode, and block states get interned by the state definition, so one state object means one string and a cache hit gives you exactly what recomputing it would have. The string never leaves the method either, the only things that ever touch it are cache.get and cache.put on the model's own private map, which nothing else in the class goes near. Bounded at 4096 entries.

Left the Matrix4f on the line above it alone on purpose. It's dead weight on the cache hit path, but on a miss it gets handed to new Transformation(matrix), which keeps the reference instead of copying it, so passing out a shared or scratch matrix would get it mutated underneath a live Transformation. It's also the one allocation here the JIT has a real shot at deleting by itself once the miss branch compiles as an uncommon trap, which toString never will.


Just Dire Things [Forge]

Its level tick walks every entity in the level and calls getBlockState for each item entity. ServerChunkCache.getChunk only has a four entry cache, and every miss goes through ticket creation and a sorted set binary search, for a plain block read.

Routes the read through getChunkNow instead, which grabs an already loaded chunk without making a ticket. Falls back to the original call if the chunk isn't there.

Was 3.7% of tick.. not that much but still sum.


Punchy

This one was shitting on my 1k fps to about 200 the moment my hands were on screen, worse holding a block, and the reason is dumb.

ItemInHandRendererMixin#isResourcePackItemDefinition asks the resource manager for <namespace>:items/<item>.json. That's the 1.21.4 item definition format. 1.20.1 doesn't have that file. So unless a resource pack ships one, every single call is a guaranteed total miss, and a miss is the most expensive answer there is. FallbackResourceManager#getResourceStack allocates a .mcmeta ResourceLocation and an ArrayList, then asks every pack in that namespace, plus every child pack of every pack, whether it has the file. On a big pack the minecraft namespace has an entry for basically every mod that ships assets, so that's a few hundred pack probes to be told no.

And it's not once. It's armed by PunchyArmRenderer#renderItemInHand calling setForceVanillaDisplay right before every held item gets drawn, so it's once per frame, per hand. Then CreatureBucketAnimator goes and asks for the exact same path again.

Now missing paths get remembered on the MultiPackResourceManager instance and repeats get answered with an empty list, which is already what that method hands back for a namespace no pack provides. Only misses get cached, anything actually there resolves normally like nothing happened. Invalidation is free because createReload builds a whole new manager every time you reload resources, so a cached miss can't ever be read under a different pack set than it was measured under. Capped at 16384 paths.

Funny part is Punchy already knows how to do this. ResourcePackItemTransformResolver runs the same probe and caches both the hit and the miss against the resource manager identity. Two other places just didn't get the memo.

Tested on 2.6.2.


Goety's Delight

The cherry blossom cake handler walks every entity in every level, every tick, and does an NBT lookup on each one. 8.2 seconds of CompoundTag.contains in a 21 minute profile.

Runs the sweep every 4 ticks instead. The punishment it drives fires on 20 tick boundaries so you won't notice.

Was 3.2% of tick.


L2 Hostility

CapabilityEvents#livingTickEvent is subscribed to LivingTickEvent, so it runs for every living entity, every tick, on both sides, and calls getCapability(MobTraitCap.CAPABILITY) on every one of them. But MobTraitCap.HOLDER only ever attaches that capability where getType().is(WHITELIST) or the thing is an Enemy thats not in the blacklist tag. Players, villagers, cows, armour stands, boats, item frames, none of them can ever have it. They all still walk their entire CapabilityDispatcher every tick to get told absent, and on a modpack that dispatcher holds a provider for every mod that hangs anything off entities.

Now the first absent answer gets remembered on the entity and after that it just returns the empty result.

This one is properly behaviour identical and I like why. Forge attaches capabilities exactly once, during AttachCapabilitiesEvent when the entity is built, and theres no API anywhere that adds a provider to an entity later. reviveCaps puts back the providers it already had, it doesnt add new ones. So absent can never turn into present, its not a guess. Only other way an absent answer could lie is if you read it while the caps are invalidated, and that cant happen here because L2H only reaches this call inside its own isAlive() branch. A present answer never gets cached at all, it goes through untouched, so the trait tick path is exactly as it was.

Tested on 2.5.19.


Terrablender

TerraBlender wraps every mod's surface rules in a namespace dispatcher, so a mod's rules only get applied inside that mod's own biomes. W idea, but the dispatch runs per block, per column, the whole time surfaces are being built. Every single call allocates a lambda, allocates an Optional, and does two string keyed map lookups, all to answer a question that depends on nothing except which biome you're in..

Now it resolves that once and caches it against the biome, and reuses it until the biome actually changes. Same rule picked, same fallback when a biome's namespace has no rules, just without redoing the lookup for every block.

Behaviour identical and I checked it properly. I built a version that ran the old lookup and the cached one side by side and counted every time they disagreed. Ten million calls through a full world generation, zero disagreements!

Regarding Terrablender Fix
Could've port it. It goes after the same problem but the main thing it does is overwrite getNamespacedRules and throw namespacing out entirely, running every mod's surface rules in one flat list. Namespacing is the whole reason mod A's rules don't paint mod B's biomes, so that's not a fix, that's a different worldgen. An horrible approach!


FTB Chunks

The minimap render does check whether the minimap is actually enabled, or hidden, or whether you're in F3. Problem is that check sits underneath the work instead of on top of it. So every frame it was binding the minimap texture and setting two filter parameters, and every time I crossed a chunk boundary it rebuilt the entire thing. That's a 15 by 15 loop, 225 region lookups and 225 texture uploads, for a minimap that then immediately doesnt get drawn because it's turned off. I never used it.

Now the check happens before the work rather than after. If the minimap is visible nothing changes at all. If it's hidden, the mod was going to return without drawing anyway, so nothing is lost, and turning it back on just triggers one fresh upload.


Curios API

  • Bails out of the per tick handler for entity types that have no curio slots at all. Curios itself already returns an empty capability for those, so this changes nothing, it just skips the capability lookup and a couple of allocations for every living entity every tick.

  • Caches that slot lookup on the entity, stamped with a generation counter. Invalidation is exact, it happens on datapack reload and client slot sync, so it can't go stale.

  • Answers "you're not wearing that" from a per tick set instead of walking your whole curio inventory every single time. findEquippedCurio walks every slot on every call, and mods that gate abilities on a worn curio call it once per item per player per tick. One mod in my pack does it from 54 separate tick handlers, 58 call sites, all firing in the same tick, all asking about a different item. Now the inventory gets walked once per entity per tick and the misses come out of that. Only the misses. If you ARE wearing the thing, the call falls through to Curios untouched, so what comes back is exactly Curios' own answer, this can never make something up. The set is built with the same slot walk findEquippedCurio uses so a wrong miss isn't possible, and it gets thrown away on Curios' own change event plus rebuilt every tick anyway. One caveat: if a curio somehow changes without firing that event, it can read as missing for the rest of that tick, so an ability could land a tick late. Toggle is there if you ever see that.

  • Skips the client side curios tick on non players.

  • Stops the Curios render layer being attached to non player renderers at all. Artifacts bolts one onto every mob renderer, this removes them at registration instead of testing per layer per entity per frame. Zero cost per frame. Downside is curios worn by mobs and armor stands stop rendering, so turn it off if your pack cares about that.

Curios tick sits at 0.28% of tick with this ON. Before it was around 5%.. 8%.. That number is from before the per tick set was added though, so it doesn't include that one.


Terramity

The held item animation handler copies both of your held item stacks, NBT and all, as the literal first thing it does. Before it checks the tick phase. Before it checks whether you're even holding a GeckoLib item. Player tick fires twice per tick on both sides, so that's four full stack copies per player per tick per side, and on a pack like mine held items are dragging enchantment and GeckoLib NBT around with them. Now it checks the conditions first and skips the whole thing.

The entity animation handler is worse.. It's on the living tick event, so it runs for every living entity in the game, and then it goes through 45 instanceof checks in a row with no else on any of them. Every zombie, every cow, every tick, both sides, ALL 45. All 45 classes it's looking for are Terramity's own entities, so if the entity isn't one of those it can't possibly match. Now it works that out once per entity class, caches it, and bails.


Armageddon

It registers 73 separate player tick listeners, one per procedure class. Every one of them is its own event bus subscriber, so every player tick fans out to all 73, twice, on both sides. They all check the phase and bail, but the dispatch already happened by then.

The actual problem is what 54 of them do once they're in. They call findEquippedCurio to check whether you're wearing one specific item, and that walks every curio slot you have. 58 call sites total, all firing in the same tick, all asking about a different item. So if you're wearing curios you're paying something like 58 full inventory walks per tick just so the mod can find out you're not holding 58 things. That's handled by the Curios patch above, one walk instead of 58.

The other thing is the entity animation handler. Same shape as Terramity's, it's on the living tick event so it runs for every living entity in the game, and it's 30 instanceof checks in a row with no else on any of them. Every mob, every tick, both sides, all 30. All 30 classes are Armageddon's own entities, so anything that isn't one can't match. Now it figures that out once per entity class, caches it, and gets out.


Born In Chaos

This one's interesting because it's genuinely the best built MCreator mod I've looked at, and it still has the worst version of the same problem.. 😭

Bad part is the entity animation handler. It's on the living tick event so it runs for every living entity in the game, and it's 83 instanceof checks in a row with no else on any of them. 83. Every cow, every zombie, every tick, both sides, all 83, matching nothing. That's a long ass chain of checks.. Fixed.

It also has the exact same held item bug as Terramity, copying both your held stacks with their NBT before checking whether it needs to do anything. Same fix.


Bosses Rise

Not as bad as I expected overall, this one actually uses GeckoLib properly and doesn't have the giant instanceof chain the MCreator mods do. But it has the single worst per tick thingy ever!

There's a procedure that runs every player tick, on both sides, and it asks the game for every single entity in a 32x32x32 box around you. Not filtered as well, the predicate is literally "true". 

And then the loop throws away everything that isn't one of four boss mobs. The sort was NEVER EVEN needed.. its just invulnerability shit..

Now the query only asks for that mod's own entities, so the list is empty basically all the time and the stream and the sort cost nothing. Same behaviour, the four bosses it cares about are all in the package being filtered for, and the original predicate still gets applied on top.

Tested on 1.0.9.2b this is due to the newer versions being buggy.


Marium's Soulslike Weaponary

The one I could fix: it hooks the start of every entity tick and calls a helper to read a despawn timer. That helper calls getPersistentData three times, checks whether the key exists, and if it doesn't, writes a zero into it before reading it back. Reading a missing NBT int already gives you zero, so that write does nothing at all except permanently stamp a despawn_timer tag onto every single entity in your world, which then gets written to disk on every save forever. Now it just reads the value once and writes nothing.

Two more I found and couldn't touch. It puts a cancellable inject at the very start of the entity render dispatcher, so every entity, every frame, pays an allocation plus an effect lookup, all to just support one stupid invisibility effect. And it adds an interface onto Item itself, which means its own "is this one of my weapons or sum?" check is true for every item in the game, so the guard it wrote does nothing.

Neither is fixable from outside, the expensive code sits inside a mixin handler method whose name gets mangled when it's applied, so there's nowhere stable to inject. The only way would be to disable their mixin and rewrite the feature myself, which means owning their bugs too..


Kind Of Nice Weapon

The only thing wrong with it is the same generated held item bug every single MCreator mod in my pack has, copying both your held stacks with their NBT before checking whether it needs to do anything. Fourth time I've patched that exact code. It's the generator atp, not the author.


Immersive Aircraft

It draws the HUD with the vanilla fill helper, which is fine, except it draws diagonal lines by calling fill once per pixel. Two per pixel if drop shadow is on. And in 1.20.1 every one of those calls ends by flushing the entire GUI buffer, which means disabling depth test, drawing everything queued so far, and re-enabling depth test. So one hundred pixel line with a shadow is two hundred full buffer flushes and four hundred GL state changes. The pitch ladder, the compass tape and the vector indicator are all built out of those lines. It's thousands of flushes every single fucking frame.

Now the whole overlay gets wrapped in the vanilla batching helper so it flushes once at the end instead.

I checked this can't break the layering before shipping it, because normally batching reorders things. The buffer source already ends the current batch whenever the render type changes, so shapes and text still draw in the same order they always did. The only thing that gets merged is consecutive geometry of the same type, which is exactly the thousands of one pixel rectangles.


Artifacts

  • Skips the kitty slippers curios scan when the entity has nothing that hurt it. The original check could never pass without an attacker, so this is free.

  • Skips the charm of sinking curios scan when the umbrella glide check can't pass anyway. Artifacts evaluates it eagerly even when the result gets thrown away.

  • Skips the client side living tick on non players.

Oculus

Shaders render the world twice. Once for what you see, and once more from the sun's point of view to build the shadow map. Plenty of things get drawn into that second pass that cannot possibly show up in it.

This skips four of them while the shadow map is being built:

  • Sign text. Font glyphs are flat quads. They cost real draw calls and contribute nothing to a depth map.

  • Enchantment glint. The glint is a whole second pass over an item's geometry with a scrolling texture on top. Doubles the cost of every enchanted item in view, for a shimmer nobody can see in a shadow.

  • Entity name tags. Same thing as sign text.

  • Banner patterns. A fully decorated banner is up to 16 extra model draws stacked on the same cloth. The base cloth still renders so the banner keeps its shadow, the pattern layers just get skipped.

Nature's Aura

Once a second it walks every loaded chunk in every level and calls update() on each one's aura data. To even get the chunk list it goes through ObfuscationReflectionHelper, so that's a reflective Method.invoke into ChunkMap every time. Then for every single chunk it does a capability lookup, which walks that chunk's whole capability provider chain and resolves a LazyOptional, just to find an object that got made once when the chunk loaded and never changes after that.

Three things fixed here:

  • Reflection is gone, it's a direct call through an accessor now. Same method, no Method.invoke.

  • The capability lookup is now just a field on the chunk. AuraChunkProvider builds its aura chunk once and never swaps it, so there's nothing to look up a second time. The field gets cleared through a capability invalidation listener so it can't outlive the thing it points at.

  • Chunks with no drain spots and nothing to sync get skipped instead of entered. update() in that state reads the level, loops over an empty map and returns, so skipping it changes nothing. It also drops one iterator allocation per loaded chunk per second and on a normal world that's basically every chunk.

Macabre - Call Of False Prophets

MCreator, 2410 classes, 679 of them are procedure classes.. It ships no mixins of its own and there's no block entity tickers in it at all, so the WHOLE runtime cost is event driven.. Three things fixed:

EntityAnimationFactory#onEntityTick is on LivingTickEvent, so every living entity in the game, every tick, both sides, and then it walks 120 sequential instanceof checks with no else on any of them, re-reading event.getEntity() before every single one. That's the longest of these generated chains I've hit, Born In Chaos had the record at 83 before. All 120 classes it tests against live in com.curseforge.macabre.entity, so an entity whose class hierarchy never enters that package can't match a single branch. Now it's one cached ClassValue lookup and out. With 200 entities loaded that's roughly 960,000 type checks a second that stop happening.

ItemAnimationFactory#animatedItems is the fifth mod I've seen with the exact same generated flaw. It copies both your held stacks, deep NBT and all, as statements one and two, before it checks the tick phase or whether either hand is even holding a GeckoLib item. PlayerTickEvent fires at START and END on both sides so that's four throwaway deep copies per player per tick per side. Checked the bytecode on this one, the method has a single return right at the end and the entire body sits inside phase == START and a hand holding a GeoItem, so cancelling on exactly that condition is identical.

The big one. Macabre re-sends an entire variable object over the network every time any one field on it gets assigned, and its per player tick procedures assign several times in a row with nothing guarding them. NumberAbilityProcedure randomises five world variables and calls syncData after each one, BosstickresetProcedure does a sixth, and every one of those is a PacketDistributor.ALL broadcast of the whole twelve field compound to every player on the server. All six run once per player per tick. So with P players that's 6P packets built and 6P squared packet writes every tick. On top of that BloodArmorAttributesProcedure calls syncPlayerVariables six times per player per tick, once per armour set, in the matched branch AND the unmatched one, so wearing nothing costs you the same six eighteen field packets as wearing a full set. Now it queues the send keyed on the variable object and does exactly one at the end of the server tick. MapVariables.get hands back the overworld's one shared SavedData for everybody, so the world variable broadcasts collapse to a single one per tick for the whole server no matter how many people are on. Four players goes from 120 packet writes a tick to 8. Ten players goes from 660 to 20.

Only the SimpleChannel#send call is wrapped there, not the method, so setDirty still runs and the data saves exactly like it did. The message objects hold a reference to the variable object and serialise it when the packet actually gets written, so the one deferred packet carries what the last of the originals would've carried, and every packet from a single server tick got applied by the client in order inside one client tick anyway. One narrow thing it changes: a field assigned after the last sync call of a tick, by code that doesn't sync itself, reaches the client one tick earlier than it used to. Fresher direction rather than staler, but it is a difference. macabre.coalesceVariableSync if you want a packet per assignment back.

Tested on 0.8.4.


Caelus API

Returns false straight away for non players that aren't already fall flying. Caelus always returns false in that state anyway, so this just skips the attribute lookup and the chest slot elytra check for every grounded mob every tick.


Lootr

  • Skips the container conversion pass entirely while both of its queues are empty. Lootr otherwise allocates two hash sets and copies the whole pending set every single tick with nothing to do.
  • Caps how many candidates get examined per tick. With a backlog you're paying a full set copy plus a synchronized block and up to 25 hasChunk calls per entry per tick, which is the exact clog Lootr's own log warns about at 5000 entries. Lootr discards entries at 18000 ticks so a 512 budget drains a 5000 entry backlog in ten ticks with room to spare.

Xaero's Minimap

Xaero rebuilds the whole minimap every single damn frame!!

Patch I did just caps how many times per second that rebuild happens. Default is 30. Skipped frames just draw the framebuffer from the last rebuild, so the minimap is still there and still looks normal, it only refreshes at the rate you set.


Xaero's + Waystones Compatibility

This one writes your entire waypoint file to disk, synchronously, on the client thread, every time waystone data comes in from the server. And the server sends one update per waystone. So joining a world where you know fifty waystones is fifty full file writes back to back on the render thread, which is exactly the kind of thing you feel as a hitch on join.

Now it just remembers that a save is needed and does one write at the end of the client tick. Same file, same contents, once instead of fifty times.


GeckoLib

GeckoLib builds model geometry by allocating a fresh Vector4f for every single vertex it writes. A cube is 6 quads of 4 vertices, so that's 24 throwaway objects per cube, per bone, per entity, per frame. On a pack with a lot of GeckoLib mobs on screen that adds up to millions of allocations a second, and all of it is garbage the GC has to clean up!!

Neither that vector or the quad normal ever leaves the method. They get transformed, read into the vertex buffer, and dropped. So they don't need to be allocated at all.

This rewrites the vertex writer to transform straight into a reused scratch vector using mulPosition, which skips the Vector4f entirely. Same math, same output, just without the crazy gc pressure.

Note: GeckoLib's renderer methods live on an interface, and Mixin does not allow normal injections into interface default methods. The only way in is a full method overwrite, which means this cannot coexist with other mods that overwrite the same method. It automatically turns itself off if GeckoBetterFPS is installed, so you won't get a crash.


Integrated API

BeardifierMixin injects at RETURN of Beardifier#compute. Beardifier is a DensityFunction and it does NOT override fillArray, so the default fillArray resolves to one compute call per element. That makes it a per sample method on the terrain shaping pass of every chunk you generate.

Every one of those samples gets handed to EnhancedBeardifierHelper#computeDensity, which reads three coords off the context, walks the enhanced rigid iterator, rewinds it with back(Integer.MAX_VALUE), then walks the enhanced junction iterator and rewinds that one too. For any chunk that doesn't have an Integrated API jigsaw structure near it both of those lists are empty, so all of that runs to add 0.0.

Now it returns the density it was given as soon as it sees both iterators are empty.

This one is properly behaviour identical and the reasoning is nice. Both iterators are ALWAYS sitting at position 0 when the method is entered, because forStructuresInChunk builds them fresh and every single exit path of computeDensity rewinds them. So hasNext() coming back false on entry means the backing list is empty. An empty list makes the loop body unreachable AND makes the rewind a no op, so the method could never have returned anything except the density it was handed.

Tested on 1.7.4.


Elysium API

This ones HORRIBLE. Almost made me tear up man..

MultiNoiseBiomeSourceMixin injects at HEAD of getNoiseBiome, which resolves every biome cell during worldgen and backs every level.getBiome at runtime. The first statement of their handler, before any guard at all, is this.getCurrentBiome(x, y, z, sampler). That's a full duplicate of the work the method is about to do anyway. sampler.sample(x, y, z) evaluates the entire climate stack (temperature, humidity, continentalness, erosion, depth, weirdness) and allocates a SinglePointContext and a TargetPoint, then findValuePositional runs an RTree search. Then vanillas own body goes and does the exact same thing again. So every biome resolution in the overworld and the nether costs twice what it should, and the first result gets binned unless a replacement fires.

Below that, still per call, it does registryOrThrow(BIOME_REPLACER).stream().toList(), which is a stream pipeline plus a fresh ArrayList copy of the whole registry, and then replaceBiomeIfNeeded allocates a HashSet and a new Random() before it has any idea whether a replacer even exists.

And the guard saves nobody. BiomeSourceMixin does @Mixin(BiomeSource.class) implements ElysiumBiomeSource, so EVERY biome source in the game passes the instanceof, and ElysiumEvents calls setDimension for both the overworld and the nether at server start. So the whole path runs for every biome lookup in both dimensions even if your pack defines zero biome replacers.

Two things here:

  • elysiumapi.memoClimateSample is the safe half and its on by default. Both calls hit the same sampler with the same coords, back to back, on the same thread, so this keeps the last sampler, position and result per thread and answers the second one from that. Behaviour identical because sample is a pure function of the sampler and the three coords, so a memo keyed on exactly those can't return anything a recompute wouldn't. Per thread so worldgen threads never share an entry. This kills the climate stack half of the duplication. The second RTree descent still happens, so its about half the waste, not all of it.

  • elysiumapi.skipUnusedBiomeReplacerLookup is the aggressive half. When no replacer of either kind is registered, replaceBiomeIfNeeded walks two empty lists and hands its input straight back, so the comparison is always equal, setReturnValue never gets called, and the computed biome is never actually READ, only compared against itself. In that state any non null holder gives identical behaviour, so this hands back a remembered one instead of resolving a fresh biome. That removes the duplicate outright instead of halving it.

Ima be honest about the second one: its behaviour identical conditional on my no replacer check being complete. I read overworldBiomes, netherBiomes and the BIOME_REPLACER registry, which are the only three sources replaceBiomeIfNeeded looks at in 1.1.3. If a later version adds a fourth source this would start hiding it. The check is re-run every call rather than cached so a datapack reload takes effect immediately, and it only applies with TerraBlender installed because the other branch is merged into MultiNoiseBiomeSource and has no injection point. If worldgen ever looks wrong turn that one off first, the safe half can stay on by itself.

Tested on 1.1.3.


Echelon - Linggango Variant

ForgeTieredAttributeSubscriber#onItemAttributeModifiers is subscribed to Forge's ItemAttributeModifierEvent, which fires from inside ItemStack#getAttributeModifiers. So it runs on equipment changes, on every tooltip frame, and every single time any other mod asks a stack for its modifiers, which on a pack like mine is a lot.

For each attribute on a tiered item it derives a stable modifier id like this:

UUID.nameUUIDFromBytes((attributeId + "_" + slot.getName() + "_" + salt).getBytes(UTF_8))

I disassembled the JDK to make sure I wasn't imagining it. UUID.nameUUIDFromBytes calls MessageDigest.getInstance("MD5") and runs a fresh digest on EVERY invocation. Its not memoised anywhere. So thats a JCA provider lookup, a MessageDigest instantiation and an MD5 hash, per attribute, per call, plus the string concat and the byte array feeding it, all to produce a value that never changes for the life of the item. MD5 setup is microseconds, not nanoseconds, and it multiplies by attributes per item times however often anything touches getAttributeModifiers.

Cached now. Behaviour identical, nameUUIDFromBytes is a pure function of its input, MD5 of the bytes with the version and variant bits stamped in, so a content keyed memo returns exactly what recomputing would. The key holds the raw bytes rather than a decoded string so it stays exact even if that call site ever passes something that isn't well formed UTF-8. Bounded at 4096 entries, and since the salt is the items own TierUUID the key space grows with distinct item instances rather than tiers, so on overflow it just drops the map. The hot set is whatever gear you actually have equipped or hovered and it refills on the next call.


Vanilla - Item Rendering

When you drop items on the floor, the game draws the item model more than once so the pile looks chunky. A stack of 64 gets drawn five times. Five full model renders, for one item entity, every frame. And if you're running shaders, all five get drawn again for the shadow map, so that's ten renders of one dropped stack per frame.

Now it's capped. Default is 1, you can set it up to 5 or set 0 to get vanilla piles back. Nothing but the render loop reads that number so this is purely cosmetic, big stacks just look flatter on the ground.


Config

Everything has its own toggle in config/collections_of_optimizations-common.toml. There's a master general.enabled if you want to B/A (before/after) test the whole thing

Screenshots

Gallery

This project has no gallery images yet.

Versions

Files

Relations

Project Relations

More like this

Similar Mods

Suggestions use data such as tags, dependencies, dependents, descriptions, titles, and more to rank how much they overlap with this mod.

On ModDex

Community snapshot

0
Ratings
0
Followers
0
In stacks

By the numbers

Statistics

~11,000
Downloads
Last Updated
Created
Last synced
When ModDex last fetched this project from CurseForge or Modrinth. Every project is re-checked on a schedule, and any project that ships a new file is synced automatically within hours of the release.
New file updates sync automatically
How syncing works