docs: durable sprint plan + baseline harness for perf batch #342-#348
- tests/benchmarks/sprint_perf_baseline.py: one harness, four isolated stress scenarios (anim-dispatch #342, scene-update #343, key-dispatch #344, grid-churn #348), wall-clock timed, step()-only/pure-loop (no screenshots). Doubles as a Callgrind target for per-function instruction counts. - docs/sprint-perf-342-348.md: durable plan with pre-fix baselines captured (wall-clock median/3 + Callgrind self-Ir per target function), the A/B re-measurement protocol, per-issue root cause / fix direction / files / acceptance / risk, suggested order (#348, #342, then #343/#344), and DoD. Baselines (Callgrind self-Ir, harness total 3.52B Ir): setProperty 158.6M (4.50%, #342); get_grid 72.5M (2.06%, 0% cache hit, #348); enum-ctor 19.18M over 6000 calls (#344); per-step update GetAttrString ~3.4k Ir/call (#343). Refs #342, #343, #344, #345, #348. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
ef0408e06e
commit
f0a44fe3e3
2 changed files with 249 additions and 0 deletions
116
tests/benchmarks/sprint_perf_baseline.py
Normal file
116
tests/benchmarks/sprint_perf_baseline.py
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
"""Sprint perf baseline — isolated stress scenarios for #342/#343/#344/#348.
|
||||
|
||||
Wall-clock timed, step()-only / pure-loop (NO screenshots — avoids the PNG-encode
|
||||
trap that buries engine work; see docs/profiling.md). Run BEFORE and AFTER the fixes
|
||||
and compare the stable-format lines. Also usable under `make callgrind SCRIPT=...`
|
||||
to capture function-level instruction counts for the four target hot paths in one run.
|
||||
|
||||
Usage:
|
||||
./build/mcrogueface --headless --exec tests/benchmarks/sprint_perf_baseline.py
|
||||
"""
|
||||
import mcrfpy
|
||||
from mcrfpy import automation
|
||||
import sys
|
||||
import time
|
||||
|
||||
# Scale knobs (kept modest so a Callgrind run finishes in a couple minutes).
|
||||
ANIM_TARGETS = 500 # frames, each animating 2 properties => 1000 active animations
|
||||
ANIM_STEPS = 200 # step() calls (animations have long duration -> stay active)
|
||||
UPDATE_STEPS = 5000 # bare-scene step() calls (isolates PyScene::update overhead)
|
||||
KEY_EVENTS = 3000 # keyDown/keyUp pairs (isolates on_key enum construction)
|
||||
GRID_DIM = 120 # NxN grid
|
||||
GRID_PASSES = 5 # full-grid at()/set() sweeps
|
||||
|
||||
|
||||
def _report(name, ops, seconds):
|
||||
us = (seconds / ops) * 1e6 if ops else 0.0
|
||||
print(f"BASELINE | {name:22s} | {ops:8d} ops | {seconds*1000:9.2f} ms | "
|
||||
f"{us:8.3f} us/op")
|
||||
|
||||
|
||||
def bench_anim_dispatch():
|
||||
"""#342 — Animation::applyValue -> setProperty(std::string,...) strcmp cascade,
|
||||
per active animation per frame."""
|
||||
scene = mcrfpy.Scene("anim")
|
||||
ui = scene.children
|
||||
frames = []
|
||||
for i in range(ANIM_TARGETS):
|
||||
f = mcrfpy.Frame(pos=(i % 400, (i * 3) % 400), size=(10, 10))
|
||||
ui.append(f)
|
||||
# long duration so the animation never completes during the run
|
||||
f.animate("x", 9999.0, 1000.0, mcrfpy.Easing.LINEAR)
|
||||
f.animate("opacity", 0.1, 1000.0, mcrfpy.Easing.LINEAR)
|
||||
frames.append(f)
|
||||
mcrfpy.current_scene = scene
|
||||
|
||||
t0 = time.perf_counter()
|
||||
for _ in range(ANIM_STEPS):
|
||||
mcrfpy.step(0.016)
|
||||
dt = time.perf_counter() - t0
|
||||
# ops = animation-value applications = active_anims * steps
|
||||
_report("342_anim_dispatch", ANIM_TARGETS * 2 * ANIM_STEPS, dt)
|
||||
|
||||
|
||||
def bench_scene_update():
|
||||
"""#343 — PyScene::update GetAttrString('update') every frame on a plain scene."""
|
||||
scene = mcrfpy.Scene("bare")
|
||||
mcrfpy.current_scene = scene
|
||||
t0 = time.perf_counter()
|
||||
for _ in range(UPDATE_STEPS):
|
||||
mcrfpy.step(0.016)
|
||||
dt = time.perf_counter() - t0
|
||||
_report("343_scene_update", UPDATE_STEPS, dt)
|
||||
|
||||
|
||||
def bench_key_dispatch():
|
||||
"""#344 — on_key rebuilds Key/InputState IntEnum members per event."""
|
||||
scene = mcrfpy.Scene("keys")
|
||||
count = [0]
|
||||
|
||||
def on_key(key, state):
|
||||
count[0] += 1
|
||||
scene.on_key = on_key
|
||||
mcrfpy.current_scene = scene
|
||||
|
||||
t0 = time.perf_counter()
|
||||
for _ in range(KEY_EVENTS):
|
||||
automation.keyDown('a')
|
||||
automation.keyUp('a')
|
||||
dt = time.perf_counter() - t0
|
||||
_report("344_key_dispatch", KEY_EVENTS * 2, dt)
|
||||
if count[0] == 0:
|
||||
print(" WARN: on_key never fired — key injection did not reach the handler")
|
||||
|
||||
|
||||
def bench_grid_churn():
|
||||
"""#348 — get_grid re-allocates a Grid wrapper + weakref per cell access."""
|
||||
color_layer = mcrfpy.ColorLayer(z_index=-1, name="c")
|
||||
grid = mcrfpy.Grid(grid_size=(GRID_DIM, GRID_DIM), pos=(0, 0), size=(800, 800),
|
||||
layers=[color_layer])
|
||||
col = mcrfpy.Color(20, 20, 20, 255)
|
||||
t0 = time.perf_counter()
|
||||
ops = 0
|
||||
for _ in range(GRID_PASSES):
|
||||
for x in range(GRID_DIM):
|
||||
for y in range(GRID_DIM):
|
||||
c = grid.at(x, y)
|
||||
c.walkable = True
|
||||
color_layer.set((x, y), col)
|
||||
ops += 1
|
||||
dt = time.perf_counter() - t0
|
||||
_report("348_grid_churn", ops, dt)
|
||||
|
||||
|
||||
def main():
|
||||
print(f"=== sprint perf baseline (anim={ANIM_TARGETS}x2/{ANIM_STEPS}, "
|
||||
f"update={UPDATE_STEPS}, keys={KEY_EVENTS}, grid={GRID_DIM}^2x{GRID_PASSES}) ===")
|
||||
bench_anim_dispatch()
|
||||
bench_scene_update()
|
||||
bench_key_dispatch()
|
||||
bench_grid_churn()
|
||||
print("=== done ===")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
sys.exit(0)
|
||||
Loading…
Add table
Add a link
Reference in a new issue