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:
1 parent
d80f85b3f6
commit
611a1a5952
102 files changed
+25853
No files matched your search
@@ -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()
|
||||
Reference in new issue
Block a user