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.
This commit is contained in:
parent
98a9497a6c
commit
59e722166a
13 changed files with 2215 additions and 0 deletions
144
tests/benchmarks/fov_opt_bench.py
Normal file
144
tests/benchmarks/fov_opt_bench.py
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
"""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)
|
||||
Loading…
Add table
Add a link
Reference in a new issue