Skip to content

Optimize network element and cable cluster hot paths - #1709

Open
rubensworks wants to merge 7 commits into
master-1.21-ltsfrom
claude/performance-optimizations-276gh5
Open

Optimize network element and cable cluster hot paths#1709
rubensworks wants to merge 7 commits into
master-1.21-ltsfrom
claude/performance-optimizations-276gh5

Conversation

@rubensworks

@rubensworks rubensworks commented Aug 26, 2026

Copy link
Copy Markdown
Member

Optimizations to the network hot paths, found by profiling the code paths that the GameTestsPerformance benchmarks exercise, plus one cleanup fix to those benchmarks.

Every change here was individually benchmarked, and only the ones with a measurable effect were kept. Six further optimizations were tried and dropped because they measured as no-ops; they are listed at the bottom in case they are still wanted as cleanups.

Optimizations

Only re-initialize distinct networks when a cable is removed

CableHelpers#onCableRemoved called NetworkHelpers#initNetwork for every side that the removed cable was connected to. Those sides are usually still connected to each other via another path, so this walked the same cable cluster and tore down and re-derived all of its network elements up to six times for a single cable removal.

Sides that already ended up in one of the networks that were just created are now skipped, so a network is only formed once per resulting cluster. Networks that genuinely split still get one initialization each.

Resolve the part state only once per operation

Most PartNetworkElement operations resolved their part state twice: once directly, and once more inside getTarget(). Each resolution is a block entity capability lookup. The state is now resolved once and passed to getTarget(S), which is what the BlockState-based variants of these methods already did.

Cheaper part network element comparisons

PartNetworkElement#compareTo dominates the cost of adding elements to and removing them from a network, since both the element set and the updateable element map are sorted.

  • Part types are singletons, so identical part types are now detected by identity, which skips the unique name and translation key string comparisons.
  • compareTo already checks that both elements are loaded before comparing their priorities, so getPriority()'s own loaded check is redundant there. Both priorities are now read with a single part container lookup instead of three.
  • getPriority, getChannel and getId share the same single-lookup helper. This one has no measurable effect on these benchmarks (they barely exercise those methods), but it is what the priority comparison above is built on, and it is a strict 3 lookups to 1 reduction elsewhere.

Skip part path elements for cable sides without a part

PathElementTileMultipartTicking#getReachableElements constructed a PartPos and a PartTarget for all six sides of every cable it visited, even though IPartContainer#getCapability immediately discards them when there is no part on that side. Path finding visits every cable of a cluster and most cables carry no parts, so this was a lot of wasted allocation.

Benchmark fix

GameTestsPerformance#addCablesPostWarmup appends its cables just outside of the generated network cube, up to 100 blocks above it, while the cleanup after each measurement only clears the cube. Those cables were never removed, so their network stayed alive and kept being ticked while the tests that run afterwards were being measured, and it was persisted into the world save.

They are now cleared together with the cube. Measured over a full run this removes the two leftover networks of about 90 cables each that the append tests left behind: 331 to 329 persisted networks, and a 17% smaller integrateddynamics_Networks.dat, since those two are by far the largest ones.

See the note at the bottom for the wider issue this is only a small part of.

Measurements

Method: PERFORMANCE_BENCHMARK_ENABLED=true ./gradlew runGameTestServer, benchmarking each commit cumulatively, with runs/gameTestServer/world wiped before every run (see the note below on why that matters). Two runs of unmodified master bracketed the sweep to establish a noise floor.

Noise floor from those two identical-code runs: redstoneioclock_remove srv ±2.6%, redstoneioclock_append srv ±2.7%, empty_remove srv ±3.1%, idle net ±7.3%, redstoneioclock net ±6.0%. The *_appendparts server tick rows varied by ±22-32% between identical runs and are not usable at all.

Per-change effect on the metrics that are above their noise floor:

change redstoneioclock_remove srv empty_remove srv redstoneioclock_append srv idle net redstoneioclock net
baseline (master) 56.0 22.0 27.7 1.23 0.50
only re-init distinct networks 25.4 (-55%) 9.5 (-53%) 27.3 1.31 0.49
resolve part state once 24.0 10.6 24.5 (-10%) 0.91 (-30%) 0.37 (-27%)
compare part types by identity 21.5 (-10%) 10.0 21.8 (-11%) 0.91 0.37
single lookup for priorities 18.0 (-17%) 9.1 20.1 (-8%) 0.91 0.40
skip part path elements 16.4 (-7%) 8.1 (-24%) 20.1 0.89 0.42
total 56.0 -> 16.4 (-71%) 22.0 -> 8.1 (-63%) 27.7 -> 20.1 (-27%) 1.23 -> 0.89 (-28%) 0.50 -> 0.42 (-16%)

Summarised:

  • Cable removal is roughly 3x cheaper, almost entirely from the onCableRemoved change, with the comparison changes adding a further 26% on part-heavy networks.
  • Average network tick time drops by about 28% on networks with active parts, entirely from resolving the part state once.
  • The comparison changes correctly show no effect at all on the empty_* presets, which contain no parts. That null result is a useful check that the signal is real.

Tried and dropped

These were implemented and benchmarked, and each measured as a no-op. I have left them out rather than carry changes that cannot be justified, but can add any of them back as plain cleanups if wanted.

  • Skipping the invalidated elements lookup in Network#isValid when nothing is invalidated. My reasoning was that the lookup costs a logarithmic number of expensive comparisons, and that is simply wrong: TreeSet#contains on an empty set returns immediately at the null root and performs zero comparisons. Note that the set is not empty for networks restored from disk, since afterServerLoad invalidates all of their elements, so this may still be worth something in a world that has saved networks. It is worth nothing in these benchmarks.
  • Collecting connected path elements into a sorted set in PathFinder. It does let the cluster be built in linear time instead of re-sorting, but it also turns every BFS insert into an O(log n) tree insert instead of an O(1) hash insert. It moves the cost rather than removing it.
  • Caching the ResourceLocation in PartTypeBase#getUniqueName. Measured -1.7% / +1.8%, i.e. nothing, because the part type identity check above already short-circuits getUniqueName() in the common case.
  • Three micro-optimizations (hoisting repeated position/level/cable lookups in Network#deriveNetworkElements and PathElementCable#getReachableElements, and one map lookup instead of four in CableDefault#isConnected). Together these measured -9% on the empty_* presets but +3% on the redstone ones, against a 15-17% spread between repeated runs. Not resolvable.

Note on benchmark stability

Worth knowing independently of this PR: results drift upwards substantially across repeated runs that reuse the same world. Over four consecutive runs I measured redstoneioclock server tick rising by 425% and empty by 179%, with the world growing by about 27MB per run.

The cause is that a full run leaves several hundred Integrated Dynamics networks behind in the world save. These are persisted in integrateddynamics_Networks.dat, and are restored and ticked on every subsequent server start whether or not their chunks are loaded, so they accumulate permanently. Each server start also places the game test grid at a completely new location in the world, so nothing is ever overwritten.

The append cable fix in this PR removes 2 of those ~330 networks. The remaining ones come from the game tests at large, which is well outside the scope of this PR.

Two practical consequences:

  • CI is not affected between runs, since each job starts from a fresh checkout. But within a single run, the batches that run later are measured while the leftover networks of the earlier batches are still being ticked.
  • Benchmarking locally requires wiping runs/gameTestServer/world between runs, otherwise the results are not comparable.

Separately, avgServerTickTime is a mean over the server's last-100-tick ring buffer sampled at a single instant, so for the presets that do not append or remove blocks it largely reflects whatever other test batches ran nearby. avgNetworkTickTime is much more stable and is the metric worth trusting for those presets.

Testing

  • ./gradlew build passes.
  • ./gradlew runGameTestServer passes, all 891 required game tests, at every commit of this branch and on master.
  • Every commit compiles individually.

🤖 Generated with Claude Code

https://claude.ai/code/session_01PyjctZ8hPuaU9iCKRqkWjC

@coveralls

coveralls commented Aug 26, 2026

Copy link
Copy Markdown

Coverage Status

coverage: 45.545% (-1.3%) from 46.881% — claude/performance-optimizations-276gh5 into master-1.21-lts

@rubensworks
rubensworks force-pushed the claude/performance-optimizations-276gh5 branch from 5a219f7 to 840d6ca Compare August 26, 2026 19:47
claude added 7 commits August 30, 2026 18:53
CableHelpers#onCableRemoved initialized a network for every side that the
removed cable was connected to. Those sides are usually still connected to
each other via another path, so this walked the same cable cluster and tore
down and re-derived all of its network elements up to six times for a single
cable removal.

Sides that already ended up in one of the networks that were just created are
now skipped, so a network is only formed once per resulting cluster.
Networks that genuinely split still get one initialization each.
Most PartNetworkElement operations resolved their part state twice: once
directly, and once more inside getTarget(). Each resolution is a block entity
capability lookup.

The state is now resolved once and passed to getTarget(S), which is what the
BlockState-based variants of these methods already did.
PartNetworkElement#getPriority, #getChannel and #getId each resolved their
part state up to three times: once for the loaded check, once for the part
presence check in hasPartState(), and once more to obtain the state itself.

They now share a getPartStateOptional() helper that does this with a single
part container lookup.
PartNetworkElement#compareTo dominates the cost of adding elements to and
removing them from a network, since both the element set and the updateable
element map are sorted.

Part types are singletons, so identical part types are guaranteed to have an
identical unique name and translation key. Detecting them by identity avoids
both of those string comparisons.

The nested comparisons are flattened into early returns to make this
readable.
…lookup

PartNetworkElement#compareTo already checks that both elements are loaded
before comparing their priorities, so getPriority()'s own loaded check is
redundant there.

Both priorities are now read with a single part container lookup instead of
three.
PathElementTileMultipartTicking#getReachableElements constructed a PartPos and
a PartTarget for all six sides of every cable it visited, even though
IPartContainer#getCapability immediately discards them when there is no part
on that side.

Since path finding visits every cable of a cluster, and most cables carry no
parts at all, this was a significant amount of wasted allocations.
…k cube

GameTestsPerformance#addCablesPostWarmup appends its cables just outside of
the generated network cube, up to 100 blocks above it, while the cleanup after
each measurement only clears the cube itself.

Those cables were therefore never removed, so their network stayed alive for
the remainder of the server run and kept being ticked while the tests that run
afterwards were being measured, and it was persisted into the world save.

Measured over a full run, this removes the two leftover networks of about 90
cables each that the append tests left behind (331 -> 329 persisted networks,
and a 17% smaller network storage file, since those two are by far the largest
ones). Note that a full run leaves several hundred smaller networks behind in
total, so this does not by itself make repeated runs over the same world
comparable.

The appended cables are now cleared together with the cube. A bounding box
variant of NetworkGenerationHelper#clearCables is added for that, so that the
SKIP_NETWORK_INIT handling stays in one place.
@rubensworks
rubensworks force-pushed the claude/performance-optimizations-276gh5 branch from 840d6ca to 837ce95 Compare August 30, 2026 19:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants