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.
This commit is contained in:
logan committed 2026-10-14 18:15:32 -04:00
1 parent d80f85b3f6
commit 611a1a5952
102 files changed
+25853

No files matched your search

+146
View File
@@ -0,0 +1,146 @@
#!/usr/bin/env python3
"""Render data/jarscan.json into ranked markdown tables (data/jarscan-report.md)."""
import json, math, sys
d = json.load(open(sys.argv[1]))
out = open(sys.argv[2], 'w')
P = lambda *a: print(*a, file=out)
FRAME_EVENTS = ('RenderLevelStageEvent', 'RenderFrameEvent', 'RenderGuiEvent', 'RenderGuiLayerEvent',
'CustomizeGuiOverlayEvent', 'RenderLivingEvent', 'RenderPlayerEvent', 'RenderHandEvent',
'RenderArmEvent', 'RenderHighlightEvent', 'ViewportEvent', 'RenderNameTagEvent',
'RenderBlockScreenEffectEvent', 'RenderTooltipEvent', 'ComputeFovModifierEvent',
'WorldRenderEvents', 'HudRenderCallback', 'HudLayerRegistrationCallback', 'ClientGuiEvent', 'GuiDrawEvent')
TICK_EVENTS = ('ClientTickEvent', 'PlayerTickEvent', 'EntityTickEvent', 'LevelTickEvent', 'ClientTickHandler')
def base_events(r, names):
return sorted({e.split('$')[0] for e in r['events'] if e.split('$')[0] in names})
rows = []
for slug, r in d.items():
if r['dir'] != 'mods':
continue
mx = r['mixins']
client_side = r['side'] != 'server'
t = r['tex']
a = r['assets']
fi = r['frame_injectors']
rows.append(dict(
slug=slug, side=r['side'],
mix=sum(mx.values()), cmix=mx.get('client', 0),
frame=r['frame_mixins'],
risky=fi.get('Overwrite', 0) + fi.get('Redirect', 0),
fev=base_events(r, FRAME_EVENTS), tev=base_events(r, TICK_EVENTS),
atlas=t.get('atlas_px', 0), atlas_n=t.get('atlas_n', 0), anim=t.get('animated_n', 0),
ent_px=t.get('entity_px', 0), tex_mb=t.get('bytes', 0) / 1e6,
models=a.get('models', 0), bstates=a.get('blockstates', 0),
shaders=a.get('shader_files', 0), post=a.get('post_effects', 0), core=len(r['core_shader_overrides']),
geo=a.get('geckolib_files', 0), ogg_mb=a.get('ogg_bytes', 0) / 1e6, particles=a.get('particle_defs', 0),
ents=r['lang'].get('entity', 0), blocks=r['lang'].get('block', 0), items=r['lang'].get('item', 0),
jar_mb=r['jar_bytes'] / 1e6, client_side=client_side,
top=', '.join(f'{k}×{v}' for k, v in sorted(r['frame_targets'].items(), key=lambda kv: -kv[1])[:6]),
))
cl = [r for r in rows if r['client_side']]
SP = 256 # one 16x16 sprite
P('# Static client-perf surface scan')
P()
P('Generated by `scripts/jarscan.py` + `scripts/report.py` from the exact jars pinned in `mods/*.pw.toml`.')
P('All numbers are static (what the jar *contains*), not runtime cost. They tell us where cost *can* come from.')
P()
P('- **frame mixins** = mixin classes whose `@Mixin` target is on a per-frame path (LevelRenderer, GameRenderer, entity/item/block-entity renderers, Gui, ParticleEngine, blaze3d, Sodium, Iris, Camera).')
P('- **risky** = `@Overwrite`/`@Redirect` injectors inside those frame mixins (replace vanilla logic; most likely to conflict with Sodium/Iris/ModernFix).')
P('- **frame events** = NeoForge/Architectury/Fabric per-frame event types referenced by the mod’s classes.')
P('- **atlas sprites** = block+item textures in 16×16-sprite equivalents (what lands in `blocks.png-atlas`, mipmapped, in VRAM).')
P()
tot_atlas = sum(r['atlas'] for r in cl)
tot = lambda k: sum(r[k] for r in cl)
P('## Pack totals (client-loaded mods)')
P()
P('| metric | value |')
P('| --- | --- |')
P(f'| client-loaded mods | {len(cl)} (server-only: {len(rows) - len(cl)}) |')
P(f'| mixin classes (client-loaded) | {tot("mix")} |')
P(f'| frame-path mixin classes | {tot("frame")} |')
P(f'| frame-path @Overwrite/@Redirect | {tot("risky")} |')
P(f'| mods hooking ≥1 per-frame event | {sum(1 for r in cl if r["fev"])} |')
P(f'| mods hooking client/player/entity tick events | {sum(1 for r in cl if r["tev"])} |')
P(f'| block+item textures | {tot("atlas_n")} files, {tot_atlas / 1e6:.1f} Mpx ≈ {tot_atlas // SP} 16×16 sprites |')
side = 2 ** math.ceil(math.log2(math.sqrt(tot_atlas))) if tot_atlas else 0
P(f'| implied blocks atlas (lower bound, + vanilla) | ≥ {side}×{side if side * side // 2 < tot_atlas else side // 2} ; ×4 B ×1.33 mip ≈ {side * side * 4 * 1.33 / 2**20:.0f} MiB VRAM at the square size |')
P(f'| animated block/item textures | {tot("anim")} |')
P(f'| entity textures | {tot("ent_px") / 1e6:.1f} Mpx |')
P(f'| models / blockstates | {tot("models")} / {tot("bstates")} |')
P(f'| entity types (lang proxy) | {tot("ents")} |')
P(f'| blocks / items (lang proxy) | {tot("blocks")} / {tot("items")} |')
P(f'| geckolib geo+animation files | {tot("geo")} |')
P(f'| vanilla core-shader overrides | {", ".join(r["slug"] for r in cl if r["core"]) or "none"} |')
P(f'| post-effect files | {", ".join(r["slug"] + f"({r['post']})" for r in cl if r["post"]) or "none"} |')
P()
def table(title, key, cols, n=25, filt=lambda r: True, note=''):
rs = sorted((r for r in cl if filt(r) and r[key]), key=lambda r: -r[key])[:n]
P(f'## {title}')
P()
if note:
P(note)
P()
P('| mod | side | ' + ' | '.join(c[0] for c in cols) + ' |')
P('| --- | --- | ' + ' | '.join('---:' if c[2] else '---' for c in cols) + ' |')
for r in rs:
P(f'| {r["slug"]} | {r["side"]} | ' + ' | '.join(c[1](r) for c in cols) + ' |')
P()
table('Per-frame mixin surface', 'frame', [
('frame mixins', lambda r: str(r['frame']), 1),
('risky', lambda r: str(r['risky']), 1),
('all mixins', lambda r: str(r['mix']), 1),
('hottest targets', lambda r: r['top'], 0)], n=40)
P('## Per-frame event subscribers')
P()
P('| mod | side | frame events | tick events |')
P('| --- | --- | --- | --- |')
for r in sorted((r for r in cl if r['fev']), key=lambda r: (-len(r['fev']), r['slug'])):
P(f'| {r["slug"]} | {r["side"]} | {", ".join(r["fev"])} | {", ".join(r["tev"])} |')
P()
table('Block/item atlas pressure', 'atlas', [
('sprites (16² eq.)', lambda r: f'{r["atlas"] // SP:,}', 1),
('files', lambda r: str(r['atlas_n']), 1),
('animated', lambda r: str(r['anim']), 1),
('% of pack', lambda r: f'{100 * r["atlas"] / tot_atlas:.1f}', 1),
('models', lambda r: str(r['models']), 1),
('blockstates', lambda r: str(r['bstates']), 1)], n=30)
table('Animated atlas textures (uploaded every tick unless culled by Sodium)', 'anim', [
('animated', lambda r: str(r['anim']), 1)], n=20)
table('Entity content (renderer + animation load)', 'ents', [
('entity types', lambda r: str(r['ents']), 1),
('entity tex Mpx', lambda r: f'{r["ent_px"] / 1e6:.2f}', 1),
('geckolib files', lambda r: str(r['geo']), 1)], n=30)
table('Shader assets', 'shaders', [
('shader files', lambda r: str(r['shaders']), 1),
('post effects', lambda r: str(r['post']), 1),
('core overrides', lambda r: str(r['core']), 1)], n=30)
table('Sound payload', 'ogg_mb', [('ogg MB', lambda r: f'{r["ogg_mb"]:.1f}', 1)], n=15)
table('Jar size', 'jar_mb', [('MB', lambda r: f'{r["jar_mb"]:.1f}', 1)], n=20)
P('## Resource packs / shader packs')
P()
for slug, r in d.items():
if r['dir'] == 'mods':
continue
t, a = r['tex'], r['assets']
P(f'- **{slug}** ({r["side"]}): atlas sprites {t.get("atlas_px", 0) // SP:,}, entity tex {t.get("entity_px", 0) / 1e6:.2f} Mpx, '
f'models {a.get("models", 0)}, blockstates {a.get("blockstates", 0)}, CEM/EMF files {a.get("cem_files", 0)}, shader files {a.get("shader_files", 0)}')
P()