McRogueFace/tests/benchmarks/fov_opt_bench.py

144 lines
4.4 KiB
Python
Raw Normal View History

Phase 5.2: performance benchmark suite for grid/entity/FOV/pathfinding Adds 6 benchmark scripts in tests/benchmarks/ covering all 5 scenarios from Kanboard #37, plus a shared baseline helper: grid_step_bench.py 100 ent / 100x100 grid / 1000 grid.step() rounds mix of IDLE/NOISE8/SEEK/FLEE behaviors fov_opt_bench.py 100 ent / 1000x1000 grid; entity.update_visibility() (with DiscreteMap perspective writeback) vs bare grid.compute_fov() (no writeback) across FOV algorithms BASIC/SHADOW/SYMMETRIC_SHADOWCAST and radii 8/16/32 spatial_hash_bench.py entities_in_radius() at radii (1,5,10,50) x entity counts (100,1k,10k); compares against naive O(n) baseline with hit-count validation pathfinding_bench.py A* across grid sizes/densities/heuristics/weights, plus with-vs-without `collide=` collision-label comparison (0/10/100 blockers on 100x100) gridview_render_bench.py 1/2/4 GridViews on shared grid; uses automation.screenshot() to force real renders in headless mode (mcrfpy.step alone is render-stubbed) dijkstra_bench.py single-root, multi-root, mask, invert, descent _baseline.py writes baseline JSON to baseline/phase5_2/ All scripts emit JSON to stdout and write a baseline copy under tests/benchmarks/baseline/phase5_2/ for regression comparison. All run headless; pure time.perf_counter() timing for compute benches, screenshot wall-time for the render bench (start/end_benchmark would only capture the no-op headless game loop, so direct timing is used). Notable findings captured in baselines: - spatial hash: 5x to >300x speedup over naive O(n), hits validated identical - update_visibility: ~25-37 ms/entity perspective writeback overhead on 1000x1000 grid (full-grid demote+promote loop in UIEntity::updateVisibility) dominates over the actual TCOD FOV cost (~3-24 ms). Worth a follow-up issue for sparse perspective updating. - gridview render: per-view cost scales near-linearly down (~78ms total for 1, 2, or 4 views) -- the multi-view system shares state efficiently. Refs Kanboard #37.
2026-04-18 06:45:40 -04:00
"""Benchmark: per-entity FOV cost on a 1000x1000 grid (Phase 5.2 / card #37).
Measures the cost of `entity.update_visibility()` (which writes through the
DiscreteMap-backed `entity.perspective_map`, the FOV optimization landed via
#294 / commit f797120) versus a bare `grid.compute_fov(...)` call (no
per-entity bookkeeping). The delta is the cost of the perspective writeback.
Configurations:
- 100 entities on 1000x1000 grid
- radii: 8, 16, 32
- FOV algorithms: BASIC, SHADOW, SYMMETRIC_SHADOWCAST
Output: JSON to stdout; baseline copy written to ./fov_opt_bench_results.json
when run from the build/ directory.
Usage:
./mcrogueface --headless --exec ../tests/benchmarks/fov_opt_bench.py
"""
import mcrfpy
import sys
import os
import time
import json
import random
sys.path.insert(0, os.path.dirname(__file__))
import _baseline
GRID_W, GRID_H = 1000, 1000
N_ENTITIES = 100
RADII = [8, 16, 32]
ALGORITHMS = [
("BASIC", mcrfpy.FOV.BASIC),
("SHADOW", mcrfpy.FOV.SHADOW),
("SYMMETRIC_SHADOWCAST", mcrfpy.FOV.SYMMETRIC_SHADOWCAST),
]
SEED = 0x1A2B
WARMUP_ROUNDS = 1
MEASURED_ROUNDS = 3
def build_grid(w, h):
g = mcrfpy.Grid(grid_size=(w, h))
# Fully-open arena. Walls only on the perimeter.
for y in range(h):
for x in range(w):
c = g.at(x, y)
walkable = (x not in (0, w - 1)) and (y not in (0, h - 1))
c.walkable = walkable
c.transparent = walkable
return g
def place_entities(g, n, rng):
ents = []
for _ in range(n):
x = rng.randrange(1, GRID_W - 1)
y = rng.randrange(1, GRID_H - 1)
e = mcrfpy.Entity((x, y), grid=g)
ents.append(e)
return ents
def measure_update_visibility(entities, rounds):
t0 = time.perf_counter()
for _ in range(rounds):
for e in entities:
e.update_visibility()
return (time.perf_counter() - t0) / rounds
def measure_grid_compute_only(grid, entities, radius, algorithm, rounds):
# entity.x/.y are pixel coords (UIDrawable). compute_fov takes grid coords.
coords = [(e.grid_pos.x, e.grid_pos.y) for e in entities]
t0 = time.perf_counter()
for _ in range(rounds):
for (x, y) in coords:
grid.compute_fov((x, y), radius=radius, light_walls=True, algorithm=algorithm)
return (time.perf_counter() - t0) / rounds
def main():
rng = random.Random(SEED)
print(f"Building {GRID_W}x{GRID_H} grid...")
grid = build_grid(GRID_W, GRID_H)
entities = place_entities(grid, N_ENTITIES, rng)
print(f"Placed {len(entities)} entities.")
runs = []
for (aname, algo) in ALGORITHMS:
grid.fov = algo
for radius in RADII:
grid.fov_radius = radius
# Warmup (allocates perspective_map + warms TCOD caches).
for _ in range(WARMUP_ROUNDS):
for e in entities:
e.update_visibility()
with_t = measure_update_visibility(entities, MEASURED_ROUNDS)
wo_t = measure_grid_compute_only(grid, entities, radius, algo, MEASURED_ROUNDS)
with_per_us = with_t / N_ENTITIES * 1e6
wo_per_us = wo_t / N_ENTITIES * 1e6
overhead_us = with_per_us - wo_per_us
entry = {
"grid": f"{GRID_W}x{GRID_H}",
"entities": N_ENTITIES,
"algorithm": aname,
"radius": radius,
"with_perspective_round_ms": with_t * 1000.0,
"without_perspective_round_ms": wo_t * 1000.0,
"with_perspective_per_entity_us": with_per_us,
"without_perspective_per_entity_us": wo_per_us,
"perspective_overhead_per_entity_us": overhead_us,
}
runs.append(entry)
print(f" {aname:<22} r={radius:<2} "
f"compute={wo_per_us:7.2f} us/ent "
f"+perspective={with_per_us:7.2f} us/ent "
f"(overhead {overhead_us:+6.2f} us)")
out = {
"config": {
"grid": f"{GRID_W}x{GRID_H}",
"entities": N_ENTITIES,
"radii": RADII,
"algorithms": [a[0] for a in ALGORITHMS],
"warmup_rounds": WARMUP_ROUNDS,
"measured_rounds": MEASURED_ROUNDS,
"seed": SEED,
},
"runs": runs,
}
print(json.dumps(out, indent=2))
_baseline.write("fov_opt_bench.json", out)
print("DONE")
if __name__ == "__main__":
main()
sys.exit(0)