Skip to content

Add performance benchmarking setup - #217

Merged
rubensworks merged 2 commits into
master-1.21-ltsfrom
claude/integrated-crafting-benchmarking-cfvc7r
Aug 26, 2026
Merged

Add performance benchmarking setup#217
rubensworks merged 2 commits into
master-1.21-ltsfrom
claude/integrated-crafting-benchmarking-cfvc7r

Conversation

@rubensworks

Copy link
Copy Markdown
Member

Measures what Integrated Crafting adds on top of an Integrated Dynamics network: crafting interfaces, crafting job scheduling and execution, and the network's recipe index. Mirrors the setup of Integrated Dynamics and Integrated Tunnels, and reuses ID's measurement infrastructure (NetworkDiagnostics.startMeasurementWithoutPlayer, GameTestHelpersIntegratedDynamics).

⚠️ One file you need to add yourself

.github/workflows/performance.yml is missing from this PR and has to be added manually.

The session credential is a platform-injected OAuth App token without the workflow scope, so every push touching .github/workflows/** is rejected:

! [remote rejected] ... (refusing to allow an OAuth App to create or
  update workflow `.github/workflows/performance.yml` without `workflow` scope)

The GitHub API write path fails too, so the file has to come from you — via the web editor on this branch, or an SSH push. Its content is IntegratedTunnels' .github/workflows/performance.yml copied verbatim with exactly three changes:

Line Change
name: of the benchmark action Integrated Crafting Network Benchmark
BENCHMARK_DATA_DIR_PATH CyclopsMC/IntegratedCrafting/${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}/benchmarks
Commit-comment results URL https://CyclopsMC.github.io/cyclops-performance-results/CyclopsMC/IntegratedCrafting/${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}/benchmarks/

I verified this mechanically by reverse-substituting the mod name and diffing against IT's file — it is otherwise byte-identical.

Everything the workflow invokes is already in this PR and verified working: .github/parse_benchmark_results.sh (not under workflows/, so it pushed fine), the game tests, and the command.

Two notes on it:

  • secrets.PAT is confirmed present and working on this repo — crowdin.yml uses it and has 411 successful scheduled runs.
  • The workflow's if: gate only matches master* and feature* branches, so it will not run on this PR's branch. Its first real run happens once this merges to master-1.21-lts.

What is measured

Eight presets on a shared cable grid (size 9 = 569 cables, 160 cells; even Y levels are solid cable planes, odd Y levels checkerboard cables with free cells). Each cell carries a part on the cable below it, so every cell is storage (chest + item interface), a crafter (crafting table + crafting interface), or a writer (crafting writer). The active presets repeat a four-cell unit over the grid, giving 40 units.

Idle — the standing cost of crafting interfaces, with nothing ever requesting a craft:

Preset What it isolates
interfaces_crafting_idle 160 crafting interfaces, 1 recipe each. Recipe index: 98
interfaces_crafting_idle_recipes Same 160 interfaces, 9 recipes each — 1440 registrations instead of 160, same distinct recipe set
craft_satisfied_idle 40 crafting writers requesting an item already in storage: what a writer pays per tick to decide there is nothing to do

Active — continuously scheduling and executing crafting jobs:

Preset What it isolates
craft_simple 80 storage / 40 crafters / 40 writers, flat single-step jobs. Sustains 4.0 crafts per tick. Recipe index: 40
craft_nested 40 storage holding only logs / 80 crafters (planks recipe + result recipe) / 40 writers. Every request must resolve and schedule a dependency graph. Sustains 5.5 crafts per tick with 35–47 jobs pending on a dependency at any moment. Recipe index: 38
craft_recipe_index Identical layout and identical crafting work to craft_simple, but 9 recipes per interface — grows the index every job calculation searches from 40 to 98 while holding crafting constant

Topology churninterfaces_crafting_append (1 → 50 interfaces, one per tick) and interfaces_crafting_remove (160 → 110).

Each unit drives its own crafting chain (a wooden item crafted from planks of one species, planks crafted from a log). That is load-bearing, not cosmetic: the crafting writer aspect refuses to schedule a job for an item the network is already crafting, so identical requests would collapse the whole grid to a single chain. The pool is 58 chains plus 40 filler recipes used only to grow the index.

Validation

Every preset was validated by temporarily instrumenting the actual work sites — CraftingJobHandler.consumeAndInsertCrafting (successful crafts + quantity moved) and CraftingHelpers.calculateAndScheduleCraftingJob (jobs scheduled) — and sampling 11 times across the 600-tick measurement window.

This caught a benchmark that measured nothing. In the first working draft, every active preset did exactly one craft per writer during warmup and then stalled permanently, while still producing a plausible non-zero tick time. Cause: storage chests were filled to all 27 slots. A crafting interface buffers its results until it can push them into network storage and stops ticking its jobs entirely while that buffer is non-empty, so a full network jams every interface after a single craft — outputBuffer stayed at 40, jobs never cleared, isCrafting stayed true, and no writer ever requested again. Storage chests now deliberately fill only 12 of 27 slots.

Final counters:

Preset crafts @0 / @300 / @600 Δ 1st half Δ 2nd half Verdict
craft_simple 800 / 2000 / 3200 +1200 +1200 exactly linear
craft_recipe_index 800 / 2000 / 3200 +1200 +1200 exactly linear
craft_nested 1100 / 2756 / 4427 +1656 +1671 linear (0.9% drift)
5 idle / churn presets 0 / 0 / 0 0 0 exactly zero

The zero controls prove the counters produce no false positives. craft_nested holding 35–47 dependency-pending jobs at every sample shows nesting genuinely sustains rather than degrading as intermediates accumulate. All instrumentation was removed before committing.

Part activation is now a permanent assertion rather than a one-off check: after warmup every preset verifies that no generated part is deactivated or has an aspect error (isDeactivated(), getErrors(activeAspect)), and that every filled recipe slot of every crafting interface is valid. A part that silently fails to activate still costs tick time, which would quietly turn a preset back into an expensive no-op.

Timing

Run Wall clock
./gradlew runGameTestServer (49 tests, benchmarks skipped) 18.4 s
PERFORMANCE_BENCHMARK_ENABLED=true ./gradlew runGameTestServer ~65 s

Well under the budget, so EXECUTION_SECONDS is set to 30 (600 ticks) rather than the 10 used by Integrated Tunnels, which keeps run-to-run variance on network tick time within a few percent: craft_simple 3.13 / 3.15, craft_nested 5.18 / 5.36, craft_recipe_index 2.83 / 2.78 across two runs.

Two caveats worth knowing

  • The idle presets are a weak signal for the action's relative 250% threshold. ID's ingredient observers are change-driven and an idle crafting interface does very little, so these presets legitimately sit at 0.2–0.6 ms, and interfaces_crafting_idle_recipes (0.24) reads lower than interfaces_crafting_idle (0.51). A small absolute regression can trip the threshold, and a small absolute regression can also hide below it. This is documented in PERFORMANCE_BENCHMARKING.md rather than papered over; the active presets are the ones to watch.
  • interfaces_crafting_remove reports ~56 ms server tick time, by design — the snapshot is taken immediately after the churn, so it includes the block updates and network re-initialisations the removal causes. That is the signal, but it will look alarming next to every other preset on the chart.

I also dropped a preset I had written: craft_simple_nonblocking produced byte-identical counters to craft_simple, because non-blocking mode is a no-op for amount-1 jobs. Making it meaningful needs batched RECIPE_CRAFT jobs, which would exhaust storage within the measurement window.

Also included

  • /integratedcrafting generatecrafting <preset> <size> exposes every preset for manual profiling in a real world, plus clear
  • PERFORMANCE_BENCHMARKING.md documents the grid layout, all eight presets, and every chosen constant with its rationale — including a note for future contributors on validating a new preset at the work site rather than trusting a non-zero number
  • benchmark_results.json added to .gitignore

Verification

  • ./gradlew build passes
  • ./gradlew runGameTestServer without the env var: all 49 tests pass, all 8 benchmark tests return immediately, no results file written
  • PERFORMANCE_BENCHMARK_ENABLED=true ./gradlew runGameTestServer writes runs/gameTestServer/logs/benchmark_results.txt
  • bash .github/parse_benchmark_results.sh produces valid JSON with 16 entries (2 per preset)
  • ./gradlew spotlessApply clean

🤖 Generated with Claude Code

https://claude.ai/code/session_01DF81jeWnWNR3ECHzNd9mRh


Generated by Claude Code

rubensworks and others added 2 commits August 25, 2026 18:33
Measures what Integrated Crafting adds on top of an Integrated Dynamics
network: crafting interfaces, crafting job scheduling and execution, and
the network's recipe index. Mirrors the setup of Integrated Dynamics and
Integrated Tunnels, and reuses ID's measurement infrastructure.

Adds eight benchmark presets on a shared cable grid: three idle presets
that isolate the standing cost of crafting interfaces and of crafting
writers that have nothing to do, three active presets that continuously
craft (flat, dependency-resolving, and with a 2.45x larger recipe index
at constant crafting work), and two topology churn presets.

All presets are also exposed through /integratedcrafting generatecrafting
<preset> <size> for manual profiling in a real world.

Every preset asserts after warmup that none of its parts is deactivated
or in an aspect error state, and that every filled recipe slot is valid,
so that a preset cannot silently degrade into an expensive no-op.

Storage chests are deliberately only filled two thirds: a crafting
interface buffers its results until it can push them into network
storage and stops ticking its jobs entirely while that buffer is
non-empty, so full chests jam every interface after a single craft.

Without PERFORMANCE_BENCHMARK_ENABLED, all benchmark game tests succeed
immediately without generating anything.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DF81jeWnWNR3ECHzNd9mRh
@rubensworks
rubensworks merged commit 5cb884c into master-1.21-lts Aug 26, 2026
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.

1 participant