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

191 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.