Add performance benchmarking setup - #217
Merged
rubensworks merged 2 commits intoAug 26, 2026
Merged
Conversation
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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)..github/workflows/performance.ymlis missing from this PR and has to be added manually.The session credential is a platform-injected OAuth App token without the
workflowscope, so every push touching.github/workflows/**is rejected: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.ymlcopied verbatim with exactly three changes:name:of the benchmark actionIntegrated Crafting Network BenchmarkBENCHMARK_DATA_DIR_PATHCyclopsMC/IntegratedCrafting/${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}/benchmarkshttps://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 underworkflows/, so it pushed fine), the game tests, and the command.Two notes on it:
secrets.PATis confirmed present and working on this repo —crowdin.ymluses it and has 411 successful scheduled runs.if:gate only matchesmaster*andfeature*branches, so it will not run on this PR's branch. Its first real run happens once this merges tomaster-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:
interfaces_crafting_idleinterfaces_crafting_idle_recipescraft_satisfied_idleActive — continuously scheduling and executing crafting jobs:
craft_simplecraft_nestedcraft_recipe_indexcraft_simple, but 9 recipes per interface — grows the index every job calculation searches from 40 to 98 while holding crafting constantTopology churn —
interfaces_crafting_append(1 → 50 interfaces, one per tick) andinterfaces_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) andCraftingHelpers.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 —
outputBufferstayed at 40, jobs never cleared,isCraftingstayed true, and no writer ever requested again. Storage chests now deliberately fill only 12 of 27 slots.Final counters:
craft_simplecraft_recipe_indexcraft_nestedThe zero controls prove the counters produce no false positives.
craft_nestedholding 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
./gradlew runGameTestServer(49 tests, benchmarks skipped)PERFORMANCE_BENCHMARK_ENABLED=true ./gradlew runGameTestServerWell under the budget, so
EXECUTION_SECONDSis 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_simple3.13 / 3.15,craft_nested5.18 / 5.36,craft_recipe_index2.83 / 2.78 across two runs.Two caveats worth knowing
interfaces_crafting_idle_recipes(0.24) reads lower thaninterfaces_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 inPERFORMANCE_BENCHMARKING.mdrather than papered over; the active presets are the ones to watch.interfaces_crafting_removereports ~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_nonblockingproduced byte-identical counters tocraft_simple, because non-blocking mode is a no-op for amount-1 jobs. Making it meaningful needs batchedRECIPE_CRAFTjobs, which would exhaust storage within the measurement window.Also included
/integratedcrafting generatecrafting <preset> <size>exposes every preset for manual profiling in a real world, plusclearPERFORMANCE_BENCHMARKING.mddocuments 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 numberbenchmark_results.jsonadded to.gitignoreVerification
./gradlew buildpasses./gradlew runGameTestServerwithout the env var: all 49 tests pass, all 8 benchmark tests return immediately, no results file writtenPERFORMANCE_BENCHMARK_ENABLED=true ./gradlew runGameTestServerwritesruns/gameTestServer/logs/benchmark_results.txtbash .github/parse_benchmark_results.shproduces valid JSON with 16 entries (2 per preset)./gradlew spotlessApplyclean🤖 Generated with Claude Code
https://claude.ai/code/session_01DF81jeWnWNR3ECHzNd9mRh
Generated by Claude Code