Skip to content

Re-implement backing state maps - #2883

Merged
octylFractal merged 2 commits into
masterfrom
perf/blockstates-round-two
Jan 5, 2026
Merged

Re-implement backing state maps#2883
octylFractal merged 2 commits into
masterfrom
perf/blockstates-round-two

Conversation

@octylFractal

@octylFractal octylFractal commented Jan 4, 2026

Copy link
Copy Markdown
Member

This uses a single array combined with efficiently computed indexes to avoid needing to do entire map equality comparisons or hold large hash-based tables, improving memory and CPU usage.

# master with obj2obj maps to fix a bug
Benchmark 1: java -jar fabric-server-mc.1.21.11-loader.0.18.4-launcher.1.1.1.jar --nogui
  Time (mean ± σ):      6.781 s ±  0.128 s    [User: 33.699 s, System: 1.062 s]
  Range (min … max):    6.567 s …  7.017 s    10 runs
# my changes
Benchmark 1: java -jar fabric-server-mc.1.21.11-loader.0.18.4-launcher.1.1.1.jar --nogui
  Time (mean ± σ):      6.464 s ±  0.045 s    [User: 32.450 s, System: 1.104 s]
  Range (min … max):    6.399 s …  6.529 s    10 runs

master with obj2obj maps:
heap dump with BlockType retaining 2,097,040 bytes

my changes:
heap dump with BlockType retaining 1,217,728 bytes

@octylFractal octylFractal added this to the 7.4.0 milestone Jan 4, 2026
@octylFractal octylFractal self-assigned this Jan 4, 2026
@octylFractal
octylFractal requested a review from a team as a code owner January 4, 2026 09:29
@octylFractal octylFractal added the type:performance Performance-related issue label Jan 4, 2026
@octylFractal
octylFractal force-pushed the perf/blockstates-round-two branch 2 times, most recently from fb2dfc3 to ebc6643 Compare January 4, 2026 09:32
@octylFractal
octylFractal requested a review from me4502 January 5, 2026 00:13
@octylFractal
octylFractal enabled auto-merge January 5, 2026 00:13
This uses a single array combined with efficiently computed indexes to
avoid needing to do entire map equality comparisons or hold large
hash-based tables, improving memory and CPU usage.
@octylFractal
octylFractal force-pushed the perf/blockstates-round-two branch from f326551 to 0e3c050 Compare January 5, 2026 01:02
@octylFractal
octylFractal added this pull request to the merge queue Jan 5, 2026
Merged via the queue into master with commit 2966134 Jan 5, 2026
5 checks passed
@octylFractal
octylFractal deleted the perf/blockstates-round-two branch January 5, 2026 01:36
@atyrode

atyrode commented Aug 17, 2026

Copy link
Copy Markdown

Real-world validation of this change at a scale well beyond the benchmark above, plus a question about reachability on older Minecraft lines.

We hit the pre-#2883 behaviour in production on a 292-mod NeoForge 1.21.1 server (WorldEdit 7.3.8, NeoForge 21.1.235, Java 21.0.12, generational ZGC, -Xmx6G). The pack registers 820,533 blockstates. The server ran at 100% heap, reclaimed nothing, and eventually died on NeoForge's ServerHangWatchdog.

Measured

jcmd <pid> GC.class_histogram, top entries by bytes:

bytes instances class
835,499,584 26,109,362 com.google.common.collect.ImmutableMapEntry
787,676,400 9,845,955 com.google.common.collect.SingletonImmutableBiMap
452,607,656 6,819,348 [Lcom.google.common.collect.ImmutableMapEntry;
436,105,472 6,814,148 com.google.common.collect.RegularImmutableMap
394,643,280 9,866,082 ImmutableMapEntry$NonTerminalImmutableMapEntry
393,023,456 6,809,285 [Ljava.util.Map$Entry;
50,881,856 795,029 com.google.common.collect.SparseImmutableTable
76,825,320 873,015 java.util.LinkedHashMap

That is roughly 3.4 GB of guava collections. Supporting counts line up with BlockState in 7.3.8 exactly: com.sk89q.worldedit.world.block.BlockState 820,533 instances (52,514,112 bytes), BaseBlock 820,533, LazyReference 1,031,890 of which 994,515 were already initialised, and the 873,015 LinkedHashMap matches one values map per state.

Before and after removing the mod, same server, same heap window, nothing else changed:

with WorldEdit 7.3.8 without
GC result 6144M(100%)->6144M(100%) 3496M(57%)->2074M(34%)
live-set floor 6144M, unreclaimable ~2.07 GB
ZGC allocation stalls 10,027 in 393 s 4 in 310 s idle
TPS (overall) 1.192 (838.68 ms/tick) 20.000 (9.82 ms/tick)

A second server on the same host, running the same pack on the same -Xmx6G but without WorldEdit, was healthy throughout (5800M(94%)->2450M(40%), 359 stalls in 4.3 h). That was the control that identified WorldEdit rather than the pack or the heap size.

Why this is the derived index, not the block registry

Worth separating, because earlier reports of this facet (#2488, #2658) were answered with the observation that WorldEdit must inherently store a list of every block, which is correct but accounts for a small fraction of the cost. In 7.3.8 the BlockState objects themselves are only ~52 MB across 820,533 states. The ~65x multiplier on top is the per-state neighbouring-state table:

// Neighbouring state table.
private Table<Property<?>, Object, BlockState> states;

built eagerly in populate(), which for each state walks every property and every value of that property and stores the resulting state in an ImmutableTable. Its size is therefore sum over states of sum over properties of (values(property) - 1), which is what produces 26.1 M map entries at 820 k states. That is a derived lookup index over the registry, not the registry — which is exactly what this PR replaces with a single array plus computed indexes, and why the approach here is the right one rather than a micro-optimisation.

For anyone finding this via a heap dump: FerriteCore does not help. It deduplicates vanilla's blockstate property maps, not WorldEdit's own copies, and it was active on our server throughout.

Your own numbers show BlockType retention dropping 2,097,040 → 1,217,728 bytes (~42%) on a vanilla-scale server. On a pack at 820 k states the same structural change removes gigabytes rather than kilobytes, since the cost scales with the product of states and property values. So this change is worth considerably more on heavily modded servers than the benchmark suggests.

The question

Is a backport of this to a Minecraft 1.21.1-compatible build considered feasible?

The reason for asking is that the fix is currently unreachable for anyone on that version. Following the tags: 7.3.8 is the last release targeting neoforge-minecraft = "1.21", 7.3.9 moved to 1.21.3, 7.3.19 targets 1.21.11, and 7.4.5 targets 26.2. This PR merged 2026-01-05, well after the 1.21.1 line ended, and I confirmed by reading BlockState.java at the tags that 7.3.19 still carries the old Table<Property<?>, Object, BlockState> states while 7.4.5 has the new stateListIndex / getInternalStateList() implementation. So on MC 1.21.1 the only available WorldEdit is 7.3.8, and the structure above is unavoidable there.

I appreciate that maintaining old Minecraft versions is a cost you have explicitly declined before, and I am not asking you to reopen that policy — a straight "no, use a current version" is a perfectly reasonable answer and I will take it as settled. We resolved our own incident by removing WorldEdit from that server (the work it had been installed for turned out to be impossible over RCON anyway, since region commands need a player selection context that a console does not have). I am posting the numbers mainly so the scale of what #2883 fixes is on the record, and so the next person who finds 3 GB of ImmutableMapEntry in a heap dump can find the explanation and the fix version rather than concluding it is a leak.

@octylFractal

Copy link
Copy Markdown
Member Author

Not sure why you asked when you know the answer.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type:performance Performance-related issue

Development

Successfully merging this pull request may close these issues.

4 participants