Skip to content

Alias the wildcard ingredients view when there is only one channel - #222

Merged
rubensworks merged 2 commits into
master-26-ltsfrom
perf/terminal-open
Sep 7, 2026
Merged

Alias the wildcard ingredients view when there is only one channel#222
rubensworks merged 2 commits into
master-26-ltsfrom
perf/terminal-open

Conversation

@rubensworks

@rubensworks rubensworks commented Sep 6, 2026

Copy link
Copy Markdown
Member

TerminalStorageTabIngredientComponentClient keeps one collapsed ingredients collection per channel plus one for the wildcard channel, which holds the sum over all channels. onChange recurses into the wildcard channel, so every change event is applied to both. On a single-channel network those two collections always hold exactly the same thing, so the same collection is built and updated twice. Neither copy can simply be dropped, because the wildcard channel is what a terminal shows by default.

With exactly one channel the wildcard view is now the very same object as that channel's view, and the diff is applied to it once instead of twice. The alias is broken, and an independent copy taken, as soon as a second channel appears, so multi-channel behaviour is unchanged.

Establishing and breaking the alias happens in one place, updateWildcardViewAlias, called from the two spots where the set of known channels changes. isWildcardViewAliased only reports state and getRawUnfilteredIngredientsView only looks up the map, so neither mutates as a side effect of being called.

A display bug this uncovered

The first version of this change was wrong, and in-game testing caught it.

onChange recursed into the wildcard channel before applying the diff to the channel's own collection. That was harmless while the wildcard held its own separate collection, because the recursion applied its own copy of the diff. With the collection shared it was not: the recursion reset the wildcard filtered view and rebuilt it from the shared collection, which had not received the change yet. Opening a terminal and then adding an item to a chest updated the total quantity but left the item list showing the contents from before the change.

The diff is now applied before the recursion.

Behaviour

Two rounds of verification, both against a live dev client by reaching into the running client and comparing the wildcard view against the per-channel views.

The synthetic networks below were run on this branch, on 26.1.

Four channels, 500 stacks each. The wildcard view stays independent and holds the sum:

channel 0: size=500 qty=14345
channel 1: size=500 qty=14615
channel 2: size=500 qty=14958
channel 3: size=500 qty=14523
wildcard: size=1100 qty=58441
sum over channels: qty=58441   (size is lower because collapsing merges equal stacks across channels)
wildcard is same object as channel 0 view: false

One channel, 2000 stacks. The wildcard view is the channel view, and holds the ingredients once rather than twice:

channel 0: size=2000 qty=58586
wildcard:  size=2000 qty=58586
wildcard is same object as channel 0 view: true

Feeding a change on a second channel into that same open terminal breaks the alias correctly:

before: wildcard=[2000, 58586] channel0=[2000, 58586] aliased=true
        (then 7 netherite ingots and 3 beacons arrive on channel 1)
after:  wildcard=[2000, 58596] channel0=[2000, 58586] channel1=[2, 10]
wildcard still aliased to channel 0: false
wildcard qty 58596 equals channel sum 58596: true
netherite in wildcard 59 = 52 in channel 0 plus the 7 added on channel 1

The second round, on a real Integrated Dynamics network with a player opening the terminal, was run on the 1.21 branch (#223), not here. That is where the display bug above was found. The client dev environment was already set up there and the changed regions are the same delta on both branches, so I did not repeat it on 26.1; saying so plainly rather than implying otherwise. From that round:

before: total=167  filtered(wildcard)=6  filtered(0)=6  aliased=true
        (an item is added to a chest on the network)
after:  total=178  filtered(wildcard)=7  filtered(0)=7  aliased=true

Before the ordering fix the same run gave filtered(wildcard)=5 against filtered(0)=6.

./gradlew build and ./gradlew runGameTestServer both pass on this branch (35 required tests).

Numbers

10 000 stacks, first open, loopback, medians of six and nine runs after a discarded warm-up. Both runs are against a CyclopsCore build carrying CyclopsMC/CyclopsCore#238, which is the change that actually made terminal opens fast; this PR is measured on top of it. Times in ms, client-side except the last row.

without the alias with the alias
client complete 180.5 177.0
of which apply 32.8 21.1
of which sort and filter 34.5 25.3
server main 37.9 33.9

The duplicated work is real and it goes away: applying the incoming changes costs a third less and rebuilding the sorted view a quarter less. End to end it is not visible. Client completion moves by 2%, which is inside the run-to-run spread of these measurements, and the server difference is noise since this is a client-only change.

So this is worth taking for removing a genuine duplicate and halving the client-side memory a terminal holds on a single-channel network, not for a speed claim. I would not have opened it on the timings alone.

Context

This came out of measuring the cost of opening a storage terminal for #219. The full report, including the profiles, the scaling numbers, and a recommendation to park #219, is in MEASUREMENT-2.md on the claude/measure-terminal-open-cost-lj4kn1 branch. The instrumentation used to measure this is on that branch and deliberately not on this one.

#223 is the same change against master-1.21-lts and stays open. Merge whichever suits your upmerge direction.

Note for review

There is no automated test here. The class is client-only and its constructor needs a live container and GUI state, so it cannot be reached from a unit test or from runGameTestServer, which is a dedicated server. The verification above is the substitute.

No changelog entry, as requested.


Generated by Claude Code

@coveralls

coveralls commented Sep 6, 2026

Copy link
Copy Markdown

Coverage Report for CI Build 34141482526

Coverage decreased (-0.08%) to 20.491%

Details

  • Coverage decreased (-0.08%) from the base build.
  • Patch coverage: No coverable lines changed in this PR.
  • 317 coverage regressions across 2 files.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

317 previously-covered lines in 2 files lost coverage.

File Lines Losing Coverage Coverage
org/cyclops/integratedterminals/core/terminalstorage/TerminalStorageTabIngredientComponentClient.java 278 0.0%
org/cyclops/integratedterminals/network/packet/CraftingJobFinishedToastPacket.java 39 5.45%

Coverage Stats

Coverage Status
Relevant Lines: 6927
Covered Lines: 1617
Line Coverage: 23.34%
Relevant Branches: 2272
Covered Branches: 268
Branch Coverage: 11.8%
Branches in Coverage %: Yes
Coverage Strength: 1.13 hits per line

💛 - Coveralls

The client tab keeps one collapsed ingredients collection per channel plus one
for the wildcard channel, which holds the sum over all channels. Every change
event was applied to both, so on a single-channel network the same collection
was built and updated twice, once for the channel and once for its identical
wildcard copy. The wildcard channel is what a terminal shows by default, so
neither copy could simply be dropped.

With exactly one channel the wildcard view now aliases that channel's view
rather than duplicating it. The alias is broken, and an independent copy taken,
as soon as a second channel appears, so multi-channel behaviour is unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mxjin31W1Lmq5XK1CCe84v

Copy link
Copy Markdown
Member Author

A master-1.21-lts version of this now exists as #223, since the plan is to upmerge rather than maintain both.

TerminalStorageTabIngredientComponentClient.java is byte identical between the branches, so it is the same patch applied to the same file.

Nothing was re-measured on 1.21. The terminal open harness exists only on the 26.1 measurement branch and its instrumentation spans seven files that diverge substantially between the branches, so porting it would have been hours of work to re-derive numbers for an identical file. #223 says that plainly rather than implying its figures came from 1.21.

This PR stays open. Merge whichever branch suits your upmerge direction.


Generated by Claude Code

Two problems with how the alias was handled.

isWildcardViewAliased was a query that mutated: it broke the alias and replaced
the wildcard entry of ingredientsUnsortedViews as a side effect of being asked a
question, and getRawUnfilteredIngredientsView established the alias as a side
effect of a lookup. All of that now happens in updateWildcardViewAlias, called
where the set of known channels changes. The query only reports state, and the
view getter only looks up a map.

The wildcard recursion in onChange also ran before the diff was applied. That
was harmless while the wildcard held its own collection, because the recursion
applied its own copy of the diff. With the collection shared it was not: the
recursion reset the wildcard filtered view and then rebuilt it, from the shared
collection, before the diff reached it. The terminal kept showing the contents
from before the change. The diff is now applied before the recursion.

Found by opening a terminal on a real network and adding an item to a chest
while it was open: the total updated but the item list did not.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mxjin31W1Lmq5XK1CCe84v
@rubensworks
rubensworks merged commit fb8bbe8 into master-26-lts Sep 7, 2026
3 checks passed
@rubensworks
rubensworks deleted the perf/terminal-open branch September 7, 2026 17:56
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