Files
onion-mc/analysis/client-perf/BENCH-MATRIX.md
T
logan 611a1a5952 docs: client-perf analysis, bench harness and work journal
Everything the next agent needs to run the Phase 2 optimisation loop on a
machine that can launch the client:

- analysis/client-perf/: LEVERS (ranked levers + corrections), BENCH-MATRIX
  (world, scenes, config IDs, blocks B0-B14 with decision rules),
  PLAYER-TUNING, SPAWN-TUNING, research briefs, the jar scanner and its
  output, observed default configs, and proposed-configs/ (build.py
  generates tiers and bench variants and fails on unknown keys;
  apply_variant.py applies a variant and merges options.txt).
- analysis/client-perf/bench/onion-bench/: bench-world datapack (scene
  teleports, Create grid, mob pen, sound storm, 120 s teleport tour).
- analysis/audit/: the static recipe/dependency audit, including gen_fixes.py,
  which generates the shipped Paxi datapack and was never tracked.
- phase0/: per-machine measurement harness (Steam Deck, 7700K/1060,
  6700K/1080 Ti).
- .pi/journal.md: running work log; read it first.

All of it is packwizignored, so nothing here ships to players.
2026-10-14 18:15:32 -04:00

15 KiB
Raw Blame History

Phase 2 benchmark matrix

This replaces LEVERS.md §6. It is meant to run without judgement calls: every config is a generated directory, every scene is a datapack function, and every block ends in a decision rule. The measurement protocol and machine prep are in ../../phase0/P{0,1,2}-*.md (uncapped, vsync off, p99 frametime + 1 % low, 15 s warm-up discarded, 3 runs, one variable per comparison). This file adds what phase0 leaves open: the world, the scenes, the configs and the order.

Machines: P0 Steam Deck · P1 7700K + GTX 1060 6 GB (Windows) · P2 6700K + 1080 Ti, ~8 GB free RAM.


0. Prerequisite: boot check

The pack has never booted at its current size: 217 mods after a01bc3b added Default Options, Sodium Extra, Reese's Sodium Options, BadOptimizations and Particle Core. Before any timing, launch once and check latest.log for:

  • missing deps and mixin apply failures;
  • fishermans-haven biome-modifier errors;
  • ftbteams references;
  • Default Options' Populated … with default options lines (proves the seeding works);
  • no YACL "Failed to deserialize" lines for fowlplay or crittersandcompanions (spawn tuning, SPAWN-TUNING.md);
  • a working Sodium video-settings screen (Reese's Sodium Options replaces it; SSRD injects a slider into it).

r5 also flags a reported Iris 1.8.14-beta.1 ↔ Sodium 0.8.13 mixin incompatibility on NeoForge 1.21.1, with a third-party "Iris-Sodium Compat" patch on CurseForge. Check for it with a shaderpack enabled. If the pack doesn't boot cleanly, fix that first. Bisect the five new mods before anything older.

1. Fixed inputs

Input Value
Pack one commit, recorded in every row (git rev-parse --short HEAD). The Puzzles Lib bump goes in before B0 if approved, never mid-matrix.
Bench instance a separate Prism instance per machine with the packwiz PreLaunchCommand removed, so it can't self-update mid-matrix. Mods are copied from a normal install of the recorded commit.
Java Temurin 21 on all three (phase0). Check java -XX:+UseShenandoahGC -version works (needed for J3).
World bench-world-v1.zip, built once on P2 (§2), sha256 recorded, unpacked identically on every machine
Configs analysis/client-perf/proposed-configs/out/<ID>/, generated by build.py at the recorded commit
In-game pins phase0 Step 7/8. The options.txt keys that matter are already in every bench variant (maxFps:260, enableVsync:false, renderDistance:8, simulationDistance:8). GUI scale, FOV, resolution and fullscreen are recorded per machine and never changed.
Resource packs / shaders none, unless the block says otherwise

Applying a config

With the game closed, run python3 proposed-configs/apply_variant.py <ID> <instance>/.minecraft. It copies all 10 config files (every variant has every file, so nothing from a previous variant survives) and merges options.txt key by key, so GUI scale, resolution and keybinds are kept. It also writes the ID to .minecraft/BENCH_VARIANT.txt; check it before each run. On Windows without Python, copy the config/ tree by hand and edit the listed options.txt keys in game. Default Options seeds only missing files, so it never interferes. Relaunch for every config change, even ones a mod could hot-reload. Run all of that config's scenes in one session, in the order S1 → S2 → S3 → S4 → S5 → S6.

JVM sets (Prism → Java arguments; -Xmx per block)

All four were checked to parse on OpenJDK 21.0.12.

ID Arguments (plus -Xmx)
J0 (phase0 default) -XX:+UseG1GC -XX:+ParallelRefProcEnabled -XX:MaxGCPauseMillis=200 -XX:+UnlockExperimentalVMOptions -XX:+DisableExplicitGC -XX:G1NewSizePercent=30 -XX:G1MaxNewSizePercent=40 -XX:G1HeapRegionSize=8M -XX:G1ReservePercent=20 -XX:G1HeapWastePercent=5 -XX:G1MixedGCCountTarget=4 -XX:InitiatingHeapOccupancyPercent=15 -XX:G1MixedGCLiveThresholdPercent=90 -XX:G1RSetUpdatingPauseTimePercent=5 -XX:SurvivorRatio=32 -XX:+PerfDisableSharedMem -XX:MaxTenuringThreshold=1
J1 (client G1, brucethemoose) -XX:+UseG1GC -XX:MaxGCPauseMillis=37 -XX:G1HeapRegionSize=16M -XX:+UnlockExperimentalVMOptions -XX:G1NewSizePercent=23 -XX:G1ReservePercent=20 -XX:G1MixedGCCountTarget=3 -XX:G1HeapWastePercent=20 -XX:InitiatingHeapOccupancyPercent=10 -XX:G1RSetUpdatingPauseTimePercent=0 -XX:MaxTenuringThreshold=1 -XX:GCTimeRatio=99 -XX:+DisableExplicitGC -XX:+PerfDisableSharedMem
J2 (Generational ZGC) -XX:+UseZGC -XX:+ZGenerational -XX:+DisableExplicitGC -XX:+PerfDisableSharedMem. Plain -XX:+UseZGC on JDK 21 is the old non-generational ZGC.
J3 (Shenandoah) -XX:+UseShenandoahGC -XX:+DisableExplicitGC -XX:+PerfDisableSharedMem. Not r6's -XX:ShenandoahGCMode=iu: on JDK 21 that aborts VM startup unless -XX:+UnlockExperimentalVMOptions is added (tested), and the mode is gone in later JDKs.

Default heap outside B4 (phase0): P0 -Xmx5G, P1 -Xmx8G, P2 -Xmx6G, all with J0.


2. Bench world (built once on P2)

  1. Copy bench/onion-bench/ into a new world's datapacks/ folder. Create the world: Creative, cheats on, seed onionbench, default world type. Run /function bench:setup.
  2. S2: /function bench:s2, then /function bench:s2_setup. Check that the shafts and cogwheels rotate. If the /fill-placed network didn't initialise, break and re-place one motor per row by hand. Accept when F3 shows Flywheel instances and every row turns.
  3. S3: /function bench:s3, then /function bench:s3_setup. Check the counts with /execute if entity @e[type=villager,x=1999,y=150,z=-1,dx=25,dy=8,dz=25] (expect 30).
  4. S4: /function bench:s4 (camera at 3000 172 -10, facing south). Build one Aeronautics test ship (about 5×7×15 blocks, planks + glass + 1 propeller) and assemble it so it hovers at y≈170. Copy it with a Create schematic to get 4 ships, centred at z = 32, 96, 192 and 384 (2, 6, 12 and 24 chunks away). SSRD 16 then cuts only the 384 one. Record the sub-level count Sable shows. This is the only hand-built scene. It's baked into the zip, so runs never rebuild it. To get the 0- and 1-ship states, break ships into blocks in a copy of the world: bench-world-v1-s4-0.zip and -s4-1.zip.
  5. DH pre-generation: use the upstream DH file (data/observed-default-configs/DistantHorizons.toml: SURFACE_THEN_CHUNKS, radius 256), then stand at S1 until DH's F3 generation queue reads 0. Run /function bench:tour_start with the same settings, waiting at each hop until the queue drains (re-issue bench:tour/stepN by hand). Do the same at S4. Every bench config then runs with generatorPlan = "DISABLED", so no world generation happens during measurement.
  6. Leave the world at S1, save, quit and zip it.

Scenes

ID Command What it loads Capture
S1 vista /function bench:s1 0 200 0, facing south, 20° down. Pure terrain + DH LODs 15 s warm-up + 30 s, hands off
S2 Create floor /function bench:s2 2,304 kinetic BEs (Flywheel instancing; vanilla BE path under shaders) 15 + 30 s
S2s sound storm S2 + /function bench:storm_on (stop: storm_off) 24 sounds/tick from 3 capped Create IDs (8 per ID per tick vs cap 4) 15 + 30 s, sound on
S3 mob pen /function bench:s3 30 villagers, 15 cows, 15 sheep, 10 allays carrying torches (moving LDL sources), 10 dropped torches 15 + 30 s
S4 airships /function bench:s4 0 / 1 / 4 Sable sub-levels at 2–24 chunks 15 + 30 s
S5 tour /function bench:s1, wait 10 s, then /function bench:tour_start 24 teleports of 160 blocks every 5 s along +x: chunk + LOD streaming, GC churn all 120 s, no warm-up discard
S6 GUI at S1, press E with the JEI overlay open; mouse parked over the JEI list GUI / JEI / tooltip path 15 + 30 s

3. Config IDs

Generated by proposed-configs/build.py. out/MANIFEST.md lists every changed key. C0 = every file at upstream default; C1 = the proposed pack default (= mid tier). All other IDs are C1 with one change.

ID Change vs C1
C0 everything back to upstream (DH 256 / SSAO on / 4 threads, Flywheel SMOOTH, LDL fancy, SPR 32/4, mip 4, blend 2, ModernFix dynres off, …)
C1-deck, C1-high the tier deltas from proposed-configs/README.md §2
dh-r256 / dh-r192 / dh-r64 DH LOD radius (C1 = 128)
dh-off C1, then DH options → Enable Rendering off after each launch. That toggle is GUI-only and never written to the file.
dh-ssao / dh-thr4 / dh-qlow SSAO on / 4 DH threads / DH's LOW quality preset at radius 128
fw-flat / fw-smooth Flywheel lightSmoothness (C1 = TRI_LINEAR)
mf-dynres-off ModernFix dynamic_resources=false
ssrd-32 / -64 / -128 SSRD physicsRenderDistance (C1 = 16)
sable-noshade Sable sub_level_dynamic_shading=false
emf-lod20 / emf-lod8 EMF LOD 20 + Average (= upstream) / LOD 8 + retainDetailOnLowFps=false
ldl-fancy / ldl-fastest LambDynamicLights mode (C1 = fast); fastest also turns entity light sources off
spr-up / spr-strict / spr-nocap SPR rays 32/4 (upstream) / strict_occlusion=true / Create sound caps removed
sod-thr3 Sodium chunk_builder_threads=3 (C1 = 0 = auto = 2)
opt-blend2 / opt-blend0 / opt-mip4 biome blend 5×5 / off; mipmap 4

4. Blocks, in run order

"Win" means the median p99 frametime improves by more than that machine's B0 spread, and the 1 % low doesn't get worse by more than the spread. Ties keep the upstream value (less change). FPS floors for tier picks: P0 1 % low ≥ 30, P1/P2 1 % low ≥ 60.

Block Machines Configs (× JVM) Scenes Records Decision rule
B0 noise floor all C0 ×3 sessions S1 p99 / 1 % spread Defines each machine's threshold. P2 also checks MemAvailable drift (phase0 Step 4).
B1 baseline all C0 S1–S6 + Game took N seconds to start (ModernFix log line), the Created: …blocks.png-atlas line, /sparkc heapsummary after S1 loads, VRAM peak (P1/P2) Reference numbers for everything below
B2 composite all; tiers on their machine C1 (all), C1-deck (P0), C1-high (P2) S1–S6 as B1 If C1 isn't a win on ≥ 2 machines, stop and bisect with B3–B11 before shipping anything
B3 Distant Horizons all dh-r256, dh-r192, C1, dh-r64, dh-off S1, S5 + DH memory in F3 Radius per tier = the largest radius meeting the FPS floor on S1 and S5: P0 → deck, P1 → default, P2 → high
P0, P1 dh-ssao, dh-qlow S1 Adopt SSAO-off / LOW-quality for that tier on a win
all dh-thr4 S1, S5 Keep 2 threads unless 4 is a win on S5 and not a loss on S1
B4 heap & GC P2, then P0 C1 (C1-deck on P0) × J0, J1, J2, J3 at -Xmx6G (P2) / -Xmx5G (P0); then the winner at 4 / 5 / 6 G S5 (120 s), S2 + /sparkc gc after S5 (pause count / max / total), swap delta (P2) GC = best S5 1 % low. Heap = the smallest size within the spread of the best. Feeds PLAYER-TUNING.md §2. P1 isn't run (32 GB, not a constraint).
B5 Sable / SSRD P0, P1 world s4-0 / s4-1 / s4-4 × C1; s4-4 × ssrd-32, ssrd-64, ssrd-128, sable-noshade S4 sub-level count SSRD per tier = the largest value meeting the floor with 4 ships. sable-noshade ships only on a win and if it looks acceptable.
P1 C1, s4-4, J0 S4, 45 min (2 runs) FPS at 0 / 15 / 30 / 45 min, /sparkc heapsummary at each Decay > spread is a Sable #1011-class finding: report it, no config fix
B6 ModernFix P2, P0 C1 vs mf-dynres-off, 3 cold launches each S1, S2, S6 startup, heap after load, missing-texture check (below) Ship dynamic_resources=true only if: heap −300 MB or more or startup faster beyond spread, and zero missing textures, and no crash in 3 launches
B7 shaders P1, P2: all rows; P0: MakeUp rows only C1 with: none / MakeUp shadowless_medium / MakeUp medium / Complementary LOW / Complementary MEDIUM (profiles set in Iris' shader settings) S2, S1, S4 (s4-4) Flywheel backend line in F3 (expect off with shaders); visual check for Iris #3164 on ships Data for the owner's shader policy, no auto-decision. Report the FPS cost per profile and whether ship corruption appears with shadow profiles
B8 entities P2, P0 (P1: FA rows only) C1 / C1 + Fresh Animations / C1 + FA + Extensions / emf-lod20 + FA / emf-lod8 + FA; ldl-fancy, ldl-fastest (no FA) S3 EMF: adopt the smallest LOD that's a win with FA on. LDL: per tier, the cheapest mode that's a win. FA guidance → PLAYER-TUNING.md §4
B9 Flywheel P0, P1 fw-flat, C1, fw-smooth (no shaders) S2 Per tier: the smoothest value meeting the floor
B10 Sound Physics P2, P0 spr-up, C1, spr-strict, spr-nocap S2s Adopt each change that's a win. Listen for obvious audio breakage.
B11 vanilla / Sodium P1, P2 sod-thr3 S5, S1 3 threads only if S5 wins and S1 doesn't lose
all opt-blend2, opt-blend0 S5 Lowest blend radius that's a win, per tier
P1 opt-mip4 S1, S2 VRAM peak Mip 4 for mid/high only if VRAM peak ≤ 5.5 GB and no loss
B12a added-mod ablation P2, P0 (P1: Sodium Extra only) C1 with one of BadOptimizations / Particle Core / Sodium Extra removed from mods/ S1, S3, S2s Keep the mod unless removing it is a win (it costs FPS) or it breaks something visible. Sodium Extra + RSO stay regardless for their per-machine options.
B12b trials per mod: Gnetum (all), Better Block Entities (all, with FA on and off), Flerovium (all), Super Resolution (P0), Ixeris (P1), Iris Flywheel Compat (with shaders, B7 scenes), Cull Leaves + MidnightLib (with Better Leaves) C1 + one mod the scenes that mod targets Owner pre-approved helpful adds. Add with packwiz modrinth add only on a win with no breakage. Not More Culling: its NeoForge build declares minecraft [1.21,1.21.1). A new mod set means re-running B0 first.
B13 profile P2 the worst scene from B2 under C1 that scene /sparkc profiler start --timeout 60, upload or save the report Flag any mod over 5 % of render-thread time that isn't Sodium, Iris, DH or Flywheel
B14 spawn tuning P2 singleplayer, defaults vs the shipped spawn files river, jungle, warm ocean (10 min each) per-species and total entity counts Procedure and keep/back-off rule: SPAWN-TUNING.md §B14

Missing-texture check (B6): in creative, open JEI, search @chipped, @rechiseled and @handcrafted, scroll each list, and place a 4×4 wall of 3 connected-texture Rechiseled blocks. Any magenta/black texture or broken CTM seam fails the check.

Rough budget

Assumes ~4 min per launch (B1 measures the real figure) and ~3 min per scene for 3 × 45 s.

Block Launches (all machines) Hours
B0–B2 ~12 ~5
B3 ~20 ~4
B4 ~14 ~3
B5 ~12 + 1.5 h decay ~4
B6 12 cold launches ~2
B7–B11 ~45 ~7
Total ~25 h machine time, ~10 h per machine

5. Output

One CSV row per run, using the phase0 recording sheet plus config_id, jvm_set, xmx, scene, world_sha, pack_commit. When the matrix is done, write the winners back into proposed-configs/build.py (DEFAULT/DECK/HIGH), regenerate, and only then present the files for shipping.