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:
John McCardle 2026-04-18 06:45:40 -04:00
commit 59e722166a
13 changed files with 2215 additions and 0 deletions

View file

@ -0,0 +1,18 @@
"""Helper for Phase 5.2 benchmark scripts to write JSON baselines.
Each bench script calls `_baseline.write("name.json", out_dict)` to save its
results to `tests/benchmarks/baseline/phase5_2/<name>`. Future runs can be
diffed against these for regression detection.
"""
import json
import os
def write(filename, payload):
base = os.path.join(os.path.dirname(__file__), "baseline", "phase5_2")
os.makedirs(base, exist_ok=True)
path = os.path.join(base, filename)
with open(path, "w") as f:
json.dump(payload, f, indent=2)
print(f" baseline written: {path}")
return path

View file

@ -0,0 +1,86 @@
{
"runs": [
{
"grid": "100x100",
"kind": "multi_root",
"roots": 1,
"mean_ms": 0.7709094556048512
},
{
"grid": "100x100",
"kind": "multi_root",
"roots": 2,
"mean_ms": 0.7632468361407518
},
{
"grid": "100x100",
"kind": "multi_root",
"roots": 5,
"mean_ms": 1.200081780552864
},
{
"grid": "100x100",
"kind": "multi_root",
"roots": 20,
"mean_ms": 2.137616788968444
},
{
"grid": "100x100",
"kind": "mask",
"roots": 500,
"mean_ms": 30.424197972752154
},
{
"grid": "100x100",
"kind": "invert",
"mean_ms": 1.0323396185413003
},
{
"grid": "100x100",
"kind": "descent_step_per_call",
"mean_us": 0.4075700417160988,
"valid_per_trial": 100
},
{
"grid": "500x500",
"kind": "multi_root",
"roots": 1,
"mean_ms": 26.075413217768073
},
{
"grid": "500x500",
"kind": "multi_root",
"roots": 2,
"mean_ms": 25.83242394030094
},
{
"grid": "500x500",
"kind": "multi_root",
"roots": 5,
"mean_ms": 33.73005616012961
},
{
"grid": "500x500",
"kind": "multi_root",
"roots": 20,
"mean_ms": 78.58918677084148
},
{
"grid": "500x500",
"kind": "mask",
"roots": 12500,
"mean_ms": 18658.679087948985
},
{
"grid": "500x500",
"kind": "invert",
"mean_ms": 25.918347598053515
},
{
"grid": "500x500",
"kind": "descent_step_per_call",
"mean_us": 0.3717193566262722,
"valid_per_trial": 2500
}
]
}

View file

@ -0,0 +1,120 @@
{
"config": {
"grid": "1000x1000",
"entities": 100,
"radii": [
8,
16,
32
],
"algorithms": [
"BASIC",
"SHADOW",
"SYMMETRIC_SHADOWCAST"
],
"warmup_rounds": 1,
"measured_rounds": 3,
"seed": 6699
},
"runs": [
{
"grid": "1000x1000",
"entities": 100,
"algorithm": "BASIC",
"radius": 8,
"with_perspective_round_ms": 2769.56388001175,
"without_perspective_round_ms": 271.07496932148933,
"with_perspective_per_entity_us": 27695.638800117496,
"without_perspective_per_entity_us": 2710.7496932148933,
"perspective_overhead_per_entity_us": 24984.889106902603
},
{
"grid": "1000x1000",
"entities": 100,
"algorithm": "BASIC",
"radius": 16,
"with_perspective_round_ms": 2984.1214296563217,
"without_perspective_round_ms": 279.2910839974259,
"with_perspective_per_entity_us": 29841.214296563216,
"without_perspective_per_entity_us": 2792.910839974259,
"perspective_overhead_per_entity_us": 27048.303456588957
},
{
"grid": "1000x1000",
"entities": 100,
"algorithm": "BASIC",
"radius": 32,
"with_perspective_round_ms": 2893.4406669965633,
"without_perspective_round_ms": 295.72028065255535,
"with_perspective_per_entity_us": 28934.406669965636,
"without_perspective_per_entity_us": 2957.202806525553,
"perspective_overhead_per_entity_us": 25977.20386344008
},
{
"grid": "1000x1000",
"entities": 100,
"algorithm": "SHADOW",
"radius": 8,
"with_perspective_round_ms": 2910.704257975643,
"without_perspective_round_ms": 287.07114366504055,
"with_perspective_per_entity_us": 29107.04257975643,
"without_perspective_per_entity_us": 2870.7114366504056,
"perspective_overhead_per_entity_us": 26236.331143106025
},
{
"grid": "1000x1000",
"entities": 100,
"algorithm": "SHADOW",
"radius": 16,
"with_perspective_round_ms": 3024.507179002588,
"without_perspective_round_ms": 288.10382665445405,
"with_perspective_per_entity_us": 30245.07179002588,
"without_perspective_per_entity_us": 2881.0382665445404,
"perspective_overhead_per_entity_us": 27364.03352348134
},
{
"grid": "1000x1000",
"entities": 100,
"algorithm": "SHADOW",
"radius": 32,
"with_perspective_round_ms": 2899.84968265829,
"without_perspective_round_ms": 284.8557453447332,
"with_perspective_per_entity_us": 28998.4968265829,
"without_perspective_per_entity_us": 2848.5574534473317,
"perspective_overhead_per_entity_us": 26149.93937313557
},
{
"grid": "1000x1000",
"entities": 100,
"algorithm": "SYMMETRIC_SHADOWCAST",
"radius": 8,
"with_perspective_round_ms": 4872.0670250089215,
"without_perspective_round_ms": 2398.7115593239046,
"with_perspective_per_entity_us": 48720.67025008921,
"without_perspective_per_entity_us": 23987.115593239043,
"perspective_overhead_per_entity_us": 24733.55465685017
},
{
"grid": "1000x1000",
"entities": 100,
"algorithm": "SYMMETRIC_SHADOWCAST",
"radius": 16,
"with_perspective_round_ms": 5419.6644353602705,
"without_perspective_round_ms": 2340.30764332662,
"with_perspective_per_entity_us": 54196.64435360271,
"without_perspective_per_entity_us": 23403.076433266204,
"perspective_overhead_per_entity_us": 30793.567920336503
},
{
"grid": "1000x1000",
"entities": 100,
"algorithm": "SYMMETRIC_SHADOWCAST",
"radius": 32,
"with_perspective_round_ms": 6089.957528670008,
"without_perspective_round_ms": 2441.893618670292,
"with_perspective_per_entity_us": 60899.575286700085,
"without_perspective_per_entity_us": 24418.936186702922,
"perspective_overhead_per_entity_us": 36480.63909999716
}
]
}

View file

@ -0,0 +1,9 @@
{
"grid": "100x100",
"entities": 100,
"rounds": 1000,
"total_sec": 0.07824495097156614,
"mean_round_ms": 0.07824495097156614,
"p95_round_ms": 0.1227830071002245,
"per_entity_step_us": 0.7824495097156614
}

View file

@ -0,0 +1,48 @@
{
"runs": [
{
"views": 1,
"frames": 60,
"warmup_frames": 5,
"total_sec": 4.710599604761228,
"mean_frame_ms": 78.50999341268714,
"p95_frame_ms": 90.47448402270675,
"implied_fps": 12.737231994702995,
"per_view_frame_ms": 78.50999341268714
},
{
"views": 2,
"frames": 60,
"warmup_frames": 5,
"total_sec": 4.6525509969796985,
"mean_frame_ms": 77.54251661632831,
"p95_frame_ms": 91.92000096663833,
"implied_fps": 12.896150958678424,
"per_view_frame_ms": 38.77125830816416
},
{
"views": 4,
"frames": 60,
"warmup_frames": 5,
"total_sec": 4.727940998389386,
"mean_frame_ms": 78.7990166398231,
"p95_frame_ms": 99.88687501754612,
"implied_fps": 12.690513697281654,
"per_view_frame_ms": 19.699754159955774
}
],
"config": {
"grid": "80x80",
"frames": 60,
"warmup_frames": 5,
"view_counts": [
1,
2,
4
],
"view_pixel_size": [
320,
320
]
}
}

View file

@ -0,0 +1,969 @@
{
"config": {
"grid_sizes": [
[
100,
100
],
[
500,
500
]
],
"obstacle_densities": [
0.1,
0.3,
0.5
],
"heuristics": [
"EUCLIDEAN",
"MANHATTAN",
"CHEBYSHEV",
"DIAGONAL",
"ZERO"
],
"weights": [
1.0,
1.5,
2.0
],
"trials": 5,
"collide_blocker_counts": [
0,
10,
100
],
"collide_trials": 20
},
"runs": [
{
"grid": "100x100",
"density": 0.1,
"heuristic": "EUCLIDEAN",
"weight": 1.0,
"collide": null,
"mean_ms": 0.942350598052144,
"hits": 5,
"mean_length": 103.0
},
{
"grid": "100x100",
"density": 0.1,
"heuristic": "EUCLIDEAN",
"weight": 1.5,
"collide": null,
"mean_ms": 0.08371900767087936,
"hits": 5,
"mean_length": 104.0
},
{
"grid": "100x100",
"density": 0.1,
"heuristic": "EUCLIDEAN",
"weight": 2.0,
"collide": null,
"mean_ms": 0.13862219639122486,
"hits": 5,
"mean_length": 104.0
},
{
"grid": "100x100",
"density": 0.1,
"heuristic": "MANHATTAN",
"weight": 1.0,
"collide": null,
"mean_ms": 0.09275858756154776,
"hits": 5,
"mean_length": 106.0
},
{
"grid": "100x100",
"density": 0.1,
"heuristic": "MANHATTAN",
"weight": 1.5,
"collide": null,
"mean_ms": 0.09280364029109478,
"hits": 5,
"mean_length": 106.0
},
{
"grid": "100x100",
"density": 0.1,
"heuristic": "MANHATTAN",
"weight": 2.0,
"collide": null,
"mean_ms": 0.08861918468028307,
"hits": 5,
"mean_length": 106.0
},
{
"grid": "100x100",
"density": 0.1,
"heuristic": "CHEBYSHEV",
"weight": 1.0,
"collide": null,
"mean_ms": 2.3062639636918902,
"hits": 5,
"mean_length": 103.0
},
{
"grid": "100x100",
"density": 0.1,
"heuristic": "CHEBYSHEV",
"weight": 1.5,
"collide": null,
"mean_ms": 0.09980038739740849,
"hits": 5,
"mean_length": 105.0
},
{
"grid": "100x100",
"density": 0.1,
"heuristic": "CHEBYSHEV",
"weight": 2.0,
"collide": null,
"mean_ms": 0.07879282347857952,
"hits": 5,
"mean_length": 107.0
},
{
"grid": "100x100",
"density": 0.1,
"heuristic": "DIAGONAL",
"weight": 1.0,
"collide": null,
"mean_ms": 0.3678584238514304,
"hits": 5,
"mean_length": 103.0
},
{
"grid": "100x100",
"density": 0.1,
"heuristic": "DIAGONAL",
"weight": 1.5,
"collide": null,
"mean_ms": 0.08070121984928846,
"hits": 5,
"mean_length": 104.0
},
{
"grid": "100x100",
"density": 0.1,
"heuristic": "DIAGONAL",
"weight": 2.0,
"collide": null,
"mean_ms": 0.07753577083349228,
"hits": 5,
"mean_length": 104.0
},
{
"grid": "100x100",
"density": 0.1,
"heuristic": "ZERO",
"weight": 1.0,
"collide": null,
"mean_ms": 2.790003828704357,
"hits": 5,
"mean_length": 103.0
},
{
"grid": "100x100",
"density": 0.1,
"heuristic": "ZERO",
"weight": 1.5,
"collide": null,
"mean_ms": 2.8951774118468165,
"hits": 5,
"mean_length": 103.0
},
{
"grid": "100x100",
"density": 0.1,
"heuristic": "ZERO",
"weight": 2.0,
"collide": null,
"mean_ms": 2.86939381621778,
"hits": 5,
"mean_length": 103.0
},
{
"grid": "100x100",
"density": 0.3,
"heuristic": "EUCLIDEAN",
"weight": 1.0,
"collide": null,
"mean_ms": 0.7874952163547277,
"hits": 5,
"mean_length": 108.0
},
{
"grid": "100x100",
"density": 0.3,
"heuristic": "EUCLIDEAN",
"weight": 1.5,
"collide": null,
"mean_ms": 0.10141241364181042,
"hits": 5,
"mean_length": 114.0
},
{
"grid": "100x100",
"density": 0.3,
"heuristic": "EUCLIDEAN",
"weight": 2.0,
"collide": null,
"mean_ms": 0.09933756664395332,
"hits": 5,
"mean_length": 116.0
},
{
"grid": "100x100",
"density": 0.3,
"heuristic": "MANHATTAN",
"weight": 1.0,
"collide": null,
"mean_ms": 0.1215972239151597,
"hits": 5,
"mean_length": 121.0
},
{
"grid": "100x100",
"density": 0.3,
"heuristic": "MANHATTAN",
"weight": 1.5,
"collide": null,
"mean_ms": 0.11956898961216211,
"hits": 5,
"mean_length": 131.0
},
{
"grid": "100x100",
"density": 0.3,
"heuristic": "MANHATTAN",
"weight": 2.0,
"collide": null,
"mean_ms": 0.11518399696797132,
"hits": 5,
"mean_length": 131.0
},
{
"grid": "100x100",
"density": 0.3,
"heuristic": "CHEBYSHEV",
"weight": 1.0,
"collide": null,
"mean_ms": 1.6034855972975492,
"hits": 5,
"mean_length": 108.0
},
{
"grid": "100x100",
"density": 0.3,
"heuristic": "CHEBYSHEV",
"weight": 1.5,
"collide": null,
"mean_ms": 0.1681813970208168,
"hits": 5,
"mean_length": 110.0
},
{
"grid": "100x100",
"density": 0.3,
"heuristic": "CHEBYSHEV",
"weight": 2.0,
"collide": null,
"mean_ms": 0.10669098701328039,
"hits": 5,
"mean_length": 115.0
},
{
"grid": "100x100",
"density": 0.3,
"heuristic": "DIAGONAL",
"weight": 1.0,
"collide": null,
"mean_ms": 0.40994961746037006,
"hits": 5,
"mean_length": 108.0
},
{
"grid": "100x100",
"density": 0.3,
"heuristic": "DIAGONAL",
"weight": 1.5,
"collide": null,
"mean_ms": 0.09926019702106714,
"hits": 5,
"mean_length": 113.0
},
{
"grid": "100x100",
"density": 0.3,
"heuristic": "DIAGONAL",
"weight": 2.0,
"collide": null,
"mean_ms": 0.10138300713151693,
"hits": 5,
"mean_length": 117.0
},
{
"grid": "100x100",
"density": 0.3,
"heuristic": "ZERO",
"weight": 1.0,
"collide": null,
"mean_ms": 2.339287009090185,
"hits": 5,
"mean_length": 108.0
},
{
"grid": "100x100",
"density": 0.3,
"heuristic": "ZERO",
"weight": 1.5,
"collide": null,
"mean_ms": 2.4245378095656633,
"hits": 5,
"mean_length": 108.0
},
{
"grid": "100x100",
"density": 0.3,
"heuristic": "ZERO",
"weight": 2.0,
"collide": null,
"mean_ms": 2.389551419764757,
"hits": 5,
"mean_length": 108.0
},
{
"grid": "100x100",
"density": 0.5,
"heuristic": "EUCLIDEAN",
"weight": 1.0,
"collide": null,
"mean_ms": 1.602034829556942,
"hits": 5,
"mean_length": 143.0
},
{
"grid": "100x100",
"density": 0.5,
"heuristic": "EUCLIDEAN",
"weight": 1.5,
"collide": null,
"mean_ms": 0.3661741968244314,
"hits": 5,
"mean_length": 148.0
},
{
"grid": "100x100",
"density": 0.5,
"heuristic": "EUCLIDEAN",
"weight": 2.0,
"collide": null,
"mean_ms": 0.2342308172956109,
"hits": 5,
"mean_length": 149.0
},
{
"grid": "100x100",
"density": 0.5,
"heuristic": "MANHATTAN",
"weight": 1.0,
"collide": null,
"mean_ms": 0.2803839975968003,
"hits": 5,
"mean_length": 157.0
},
{
"grid": "100x100",
"density": 0.5,
"heuristic": "MANHATTAN",
"weight": 1.5,
"collide": null,
"mean_ms": 0.20836759358644485,
"hits": 5,
"mean_length": 168.0
},
{
"grid": "100x100",
"density": 0.5,
"heuristic": "MANHATTAN",
"weight": 2.0,
"collide": null,
"mean_ms": 0.20984318107366562,
"hits": 5,
"mean_length": 169.0
},
{
"grid": "100x100",
"density": 0.5,
"heuristic": "CHEBYSHEV",
"weight": 1.0,
"collide": null,
"mean_ms": 1.5648985747247934,
"hits": 5,
"mean_length": 143.0
},
{
"grid": "100x100",
"density": 0.5,
"heuristic": "CHEBYSHEV",
"weight": 1.5,
"collide": null,
"mean_ms": 1.0680590057745576,
"hits": 5,
"mean_length": 153.0
},
{
"grid": "100x100",
"density": 0.5,
"heuristic": "CHEBYSHEV",
"weight": 2.0,
"collide": null,
"mean_ms": 0.5100774113088846,
"hits": 5,
"mean_length": 159.0
},
{
"grid": "100x100",
"density": 0.5,
"heuristic": "DIAGONAL",
"weight": 1.0,
"collide": null,
"mean_ms": 1.2593558290973306,
"hits": 5,
"mean_length": 143.0
},
{
"grid": "100x100",
"density": 0.5,
"heuristic": "DIAGONAL",
"weight": 1.5,
"collide": null,
"mean_ms": 0.35627696197479963,
"hits": 5,
"mean_length": 156.0
},
{
"grid": "100x100",
"density": 0.5,
"heuristic": "DIAGONAL",
"weight": 2.0,
"collide": null,
"mean_ms": 0.22568658459931612,
"hits": 5,
"mean_length": 158.0
},
{
"grid": "100x100",
"density": 0.5,
"heuristic": "ZERO",
"weight": 1.0,
"collide": null,
"mean_ms": 1.7558214021846652,
"hits": 5,
"mean_length": 143.0
},
{
"grid": "100x100",
"density": 0.5,
"heuristic": "ZERO",
"weight": 1.5,
"collide": null,
"mean_ms": 1.7436427995562553,
"hits": 5,
"mean_length": 143.0
},
{
"grid": "100x100",
"density": 0.5,
"heuristic": "ZERO",
"weight": 2.0,
"collide": null,
"mean_ms": 1.7293283948674798,
"hits": 5,
"mean_length": 143.0
},
{
"grid": "500x500",
"density": 0.1,
"heuristic": "EUCLIDEAN",
"weight": 1.0,
"collide": null,
"mean_ms": 32.0626940112561,
"hits": 5,
"mean_length": 513.0
},
{
"grid": "500x500",
"density": 0.1,
"heuristic": "EUCLIDEAN",
"weight": 1.5,
"collide": null,
"mean_ms": 1.5519145876169205,
"hits": 5,
"mean_length": 522.0
},
{
"grid": "500x500",
"density": 0.1,
"heuristic": "EUCLIDEAN",
"weight": 2.0,
"collide": null,
"mean_ms": 1.4397500082850456,
"hits": 5,
"mean_length": 522.0
},
{
"grid": "500x500",
"density": 0.1,
"heuristic": "MANHATTAN",
"weight": 1.0,
"collide": null,
"mean_ms": 1.591819617897272,
"hits": 5,
"mean_length": 530.0
},
{
"grid": "500x500",
"density": 0.1,
"heuristic": "MANHATTAN",
"weight": 1.5,
"collide": null,
"mean_ms": 1.6432177973911166,
"hits": 5,
"mean_length": 546.0
},
{
"grid": "500x500",
"density": 0.1,
"heuristic": "MANHATTAN",
"weight": 2.0,
"collide": null,
"mean_ms": 1.6989499796181917,
"hits": 5,
"mean_length": 546.0
},
{
"grid": "500x500",
"density": 0.1,
"heuristic": "CHEBYSHEV",
"weight": 1.0,
"collide": null,
"mean_ms": 156.1333344085142,
"hits": 5,
"mean_length": 513.0
},
{
"grid": "500x500",
"density": 0.1,
"heuristic": "CHEBYSHEV",
"weight": 1.5,
"collide": null,
"mean_ms": 1.5195768093690276,
"hits": 5,
"mean_length": 521.0
},
{
"grid": "500x500",
"density": 0.1,
"heuristic": "CHEBYSHEV",
"weight": 2.0,
"collide": null,
"mean_ms": 1.4803750207647681,
"hits": 5,
"mean_length": 526.0
},
{
"grid": "500x500",
"density": 0.1,
"heuristic": "DIAGONAL",
"weight": 1.0,
"collide": null,
"mean_ms": 15.02777342684567,
"hits": 5,
"mean_length": 513.0
},
{
"grid": "500x500",
"density": 0.1,
"heuristic": "DIAGONAL",
"weight": 1.5,
"collide": null,
"mean_ms": 1.6023054020479321,
"hits": 5,
"mean_length": 520.0
},
{
"grid": "500x500",
"density": 0.1,
"heuristic": "DIAGONAL",
"weight": 2.0,
"collide": null,
"mean_ms": 1.6157740028575063,
"hits": 5,
"mean_length": 520.0
},
{
"grid": "500x500",
"density": 0.1,
"heuristic": "ZERO",
"weight": 1.0,
"collide": null,
"mean_ms": 97.6897893473506,
"hits": 5,
"mean_length": 513.0
},
{
"grid": "500x500",
"density": 0.1,
"heuristic": "ZERO",
"weight": 1.5,
"collide": null,
"mean_ms": 96.43948636949062,
"hits": 5,
"mean_length": 513.0
},
{
"grid": "500x500",
"density": 0.1,
"heuristic": "ZERO",
"weight": 2.0,
"collide": null,
"mean_ms": 107.46402416843921,
"hits": 5,
"mean_length": 513.0
},
{
"grid": "500x500",
"density": 0.3,
"heuristic": "EUCLIDEAN",
"weight": 1.0,
"collide": null,
"mean_ms": 57.68123983871192,
"hits": 5,
"mean_length": 557.0
},
{
"grid": "500x500",
"density": 0.3,
"heuristic": "EUCLIDEAN",
"weight": 1.5,
"collide": null,
"mean_ms": 1.858492591418326,
"hits": 5,
"mean_length": 588.0
},
{
"grid": "500x500",
"density": 0.3,
"heuristic": "EUCLIDEAN",
"weight": 2.0,
"collide": null,
"mean_ms": 1.757865771651268,
"hits": 5,
"mean_length": 595.0
},
{
"grid": "500x500",
"density": 0.3,
"heuristic": "MANHATTAN",
"weight": 1.0,
"collide": null,
"mean_ms": 1.8272274173796177,
"hits": 5,
"mean_length": 586.0
},
{
"grid": "500x500",
"density": 0.3,
"heuristic": "MANHATTAN",
"weight": 1.5,
"collide": null,
"mean_ms": 1.9388356013223529,
"hits": 5,
"mean_length": 625.0
},
{
"grid": "500x500",
"density": 0.3,
"heuristic": "MANHATTAN",
"weight": 2.0,
"collide": null,
"mean_ms": 1.9301387714222074,
"hits": 5,
"mean_length": 622.0
},
{
"grid": "500x500",
"density": 0.3,
"heuristic": "CHEBYSHEV",
"weight": 1.0,
"collide": null,
"mean_ms": 96.00871021393687,
"hits": 5,
"mean_length": 557.0
},
{
"grid": "500x500",
"density": 0.3,
"heuristic": "CHEBYSHEV",
"weight": 1.5,
"collide": null,
"mean_ms": 6.980397994630039,
"hits": 5,
"mean_length": 572.0
},
{
"grid": "500x500",
"density": 0.3,
"heuristic": "CHEBYSHEV",
"weight": 2.0,
"collide": null,
"mean_ms": 2.038404601626098,
"hits": 5,
"mean_length": 604.0
},
{
"grid": "500x500",
"density": 0.3,
"heuristic": "DIAGONAL",
"weight": 1.0,
"collide": null,
"mean_ms": 33.08099440764636,
"hits": 5,
"mean_length": 557.0
},
{
"grid": "500x500",
"density": 0.3,
"heuristic": "DIAGONAL",
"weight": 1.5,
"collide": null,
"mean_ms": 1.853386196307838,
"hits": 5,
"mean_length": 589.0
},
{
"grid": "500x500",
"density": 0.3,
"heuristic": "DIAGONAL",
"weight": 2.0,
"collide": null,
"mean_ms": 1.8388852244243026,
"hits": 5,
"mean_length": 593.0
},
{
"grid": "500x500",
"density": 0.3,
"heuristic": "ZERO",
"weight": 1.0,
"collide": null,
"mean_ms": 88.17679616622627,
"hits": 5,
"mean_length": 557.0
},
{
"grid": "500x500",
"density": 0.3,
"heuristic": "ZERO",
"weight": 1.5,
"collide": null,
"mean_ms": 92.76693619322032,
"hits": 5,
"mean_length": 557.0
},
{
"grid": "500x500",
"density": 0.3,
"heuristic": "ZERO",
"weight": 2.0,
"collide": null,
"mean_ms": 82.64447257388383,
"hits": 5,
"mean_length": 557.0
},
{
"grid": "500x500",
"density": 0.5,
"heuristic": "EUCLIDEAN",
"weight": 1.0,
"collide": null,
"mean_ms": 59.1546967625618,
"hits": 5,
"mean_length": 683.0
},
{
"grid": "500x500",
"density": 0.5,
"heuristic": "EUCLIDEAN",
"weight": 1.5,
"collide": null,
"mean_ms": 2.7708559995517135,
"hits": 5,
"mean_length": 715.0
},
{
"grid": "500x500",
"density": 0.5,
"heuristic": "EUCLIDEAN",
"weight": 2.0,
"collide": null,
"mean_ms": 4.107674001716077,
"hits": 5,
"mean_length": 733.0
},
{
"grid": "500x500",
"density": 0.5,
"heuristic": "MANHATTAN",
"weight": 1.0,
"collide": null,
"mean_ms": 2.8352454071864486,
"hits": 5,
"mean_length": 710.0
},
{
"grid": "500x500",
"density": 0.5,
"heuristic": "MANHATTAN",
"weight": 1.5,
"collide": null,
"mean_ms": 3.3035014057531953,
"hits": 5,
"mean_length": 767.0
},
{
"grid": "500x500",
"density": 0.5,
"heuristic": "MANHATTAN",
"weight": 2.0,
"collide": null,
"mean_ms": 3.7329247687011957,
"hits": 5,
"mean_length": 757.0
},
{
"grid": "500x500",
"density": 0.5,
"heuristic": "CHEBYSHEV",
"weight": 1.0,
"collide": null,
"mean_ms": 61.771947774104774,
"hits": 5,
"mean_length": 683.0
},
{
"grid": "500x500",
"density": 0.5,
"heuristic": "CHEBYSHEV",
"weight": 1.5,
"collide": null,
"mean_ms": 21.38091274537146,
"hits": 5,
"mean_length": 688.0
},
{
"grid": "500x500",
"density": 0.5,
"heuristic": "CHEBYSHEV",
"weight": 2.0,
"collide": null,
"mean_ms": 3.3959574066102505,
"hits": 5,
"mean_length": 711.0
},
{
"grid": "500x500",
"density": 0.5,
"heuristic": "DIAGONAL",
"weight": 1.0,
"collide": null,
"mean_ms": 42.92491900268942,
"hits": 5,
"mean_length": 683.0
},
{
"grid": "500x500",
"density": 0.5,
"heuristic": "DIAGONAL",
"weight": 1.5,
"collide": null,
"mean_ms": 3.1225387938320637,
"hits": 5,
"mean_length": 710.0
},
{
"grid": "500x500",
"density": 0.5,
"heuristic": "DIAGONAL",
"weight": 2.0,
"collide": null,
"mean_ms": 2.8334501897916198,
"hits": 5,
"mean_length": 721.0
},
{
"grid": "500x500",
"density": 0.5,
"heuristic": "ZERO",
"weight": 1.0,
"collide": null,
"mean_ms": 57.06864399835467,
"hits": 5,
"mean_length": 683.0
},
{
"grid": "500x500",
"density": 0.5,
"heuristic": "ZERO",
"weight": 1.5,
"collide": null,
"mean_ms": 55.27959519531578,
"hits": 5,
"mean_length": 683.0
},
{
"grid": "500x500",
"density": 0.5,
"heuristic": "ZERO",
"weight": 2.0,
"collide": null,
"mean_ms": 55.80121979583055,
"hits": 5,
"mean_length": 683.0
}
],
"collide_runs": [
{
"grid": "100x100",
"blockers": 0,
"without_collide_ms": 0.655252585420385,
"with_collide_ms": 0.6339186511468142,
"without_collide_path_len": 101.0,
"with_collide_path_len": 101.0,
"overhead_ms": -0.021333934273570776
},
{
"grid": "100x100",
"blockers": 10,
"without_collide_ms": 0.6333888624794781,
"with_collide_ms": 0.624053756473586,
"without_collide_path_len": 101.0,
"with_collide_path_len": 101.0,
"overhead_ms": -0.009335106005892158
},
{
"grid": "100x100",
"blockers": 100,
"without_collide_ms": 0.6174998939968646,
"with_collide_ms": 0.8494114445056766,
"without_collide_path_len": 101.0,
"with_collide_path_len": 102.0,
"overhead_ms": 0.23191155050881207
}
]
}

View file

@ -0,0 +1,141 @@
{
"config": {
"grid": "200x200",
"entity_counts": [
100,
1000,
10000
],
"radii": [
1,
5,
10,
50
],
"queries_per_config": 200,
"sample_query_locations": 50,
"seed": 51966
},
"runs": [
{
"entities": 100,
"radius": 1,
"queries": 200,
"spatial_per_query_us": 1.4766055392101407,
"naive_per_query_us": 14.514889917336404,
"speedup": 9.829903472460659,
"spatial_mean_hits": 0.0,
"naive_mean_hits": 0.0
},
{
"entities": 100,
"radius": 5,
"queries": 200,
"spatial_per_query_us": 1.5784951392561197,
"naive_per_query_us": 14.675830025225878,
"speedup": 9.297355221595422,
"spatial_mean_hits": 0.28,
"naive_mean_hits": 0.28
},
{
"entities": 100,
"radius": 10,
"queries": 200,
"spatial_per_query_us": 1.7352000577375293,
"naive_per_query_us": 14.739789767190814,
"speedup": 8.49457657718704,
"spatial_mean_hits": 0.94,
"naive_mean_hits": 0.94
},
{
"entities": 100,
"radius": 50,
"queries": 200,
"spatial_per_query_us": 3.2524653943255544,
"naive_per_query_us": 15.446714824065564,
"speedup": 4.749232643955206,
"spatial_mean_hits": 15.26,
"naive_mean_hits": 15.26
},
{
"entities": 1000,
"radius": 1,
"queries": 200,
"spatial_per_query_us": 3.663320094347,
"naive_per_query_us": 153.2661501551047,
"speedup": 41.83804478118501,
"spatial_mean_hits": 0.1,
"naive_mean_hits": 0.1
},
{
"entities": 1000,
"radius": 5,
"queries": 200,
"spatial_per_query_us": 2.5340047432109714,
"naive_per_query_us": 205.5685903178528,
"speedup": 81.12399586804482,
"spatial_mean_hits": 1.98,
"naive_mean_hits": 1.98
},
{
"entities": 1000,
"radius": 10,
"queries": 200,
"spatial_per_query_us": 3.4613750176504254,
"naive_per_query_us": 156.61109995562583,
"speedup": 45.245343008783,
"spatial_mean_hits": 7.94,
"naive_mean_hits": 7.94
},
{
"entities": 1000,
"radius": 50,
"queries": 200,
"spatial_per_query_us": 18.54475529398769,
"naive_per_query_us": 160.16811016015708,
"speedup": 8.6368413937543,
"spatial_mean_hits": 153.96,
"naive_mean_hits": 153.96
},
{
"entities": 10000,
"radius": 1,
"queries": 200,
"spatial_per_query_us": 8.608360076323152,
"naive_per_query_us": 1619.5069800596684,
"speedup": 188.1318817638726,
"spatial_mean_hits": 0.92,
"naive_mean_hits": 0.92
},
{
"entities": 10000,
"radius": 5,
"queries": 200,
"spatial_per_query_us": 18.44062004238367,
"naive_per_query_us": 2015.9114798298103,
"speedup": 109.31907252556947,
"spatial_mean_hits": 19.2,
"naive_mean_hits": 19.2
},
{
"entities": 10000,
"radius": 10,
"queries": 200,
"spatial_per_query_us": 32.97713526990265,
"naive_per_query_us": 1611.3787598442286,
"speedup": 48.86351548295013,
"spatial_mean_hits": 75.86,
"naive_mean_hits": 75.86
},
{
"entities": 10000,
"radius": 50,
"queries": 200,
"spatial_per_query_us": 346.19326528627425,
"naive_per_query_us": 1811.6197548806667,
"speedup": 5.232972263000557,
"spatial_mean_hits": 1583.42,
"naive_mean_hits": 1583.42
}
]
}

View file

@ -0,0 +1,124 @@
"""Benchmark: single-root, multi-root, DiscreteMap-mask Dijkstra plus invert+descent.
Usage:
./mcrogueface --headless --exec ../tests/benchmarks/dijkstra_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_SIZES = [(100, 100), (500, 500)]
ROOT_COUNTS = [1, 2, 5, 20]
MASK_DENSITY = 0.05
TRIALS = 5
SEED = 0x1234
def make_grid(w, h):
g = mcrfpy.Grid(grid_size=(w, h))
for y in range(h):
for x in range(w):
c = g.at(x, y)
c.walkable = True
c.transparent = True
return g
def random_points(w, h, n, rng):
pts = set()
while len(pts) < n:
pts.add((rng.randrange(1, w - 1), rng.randrange(1, h - 1)))
return list(pts)
def bench_compute(g, roots, trials):
total = 0.0
for _ in range(trials):
g.clear_dijkstra_maps()
t0 = time.perf_counter()
_ = g.get_dijkstra_map(roots=roots)
total += time.perf_counter() - t0
return total / trials
def bench_mask(g, mask, trials):
total = 0.0
for _ in range(trials):
g.clear_dijkstra_maps()
t0 = time.perf_counter()
_ = g.get_dijkstra_map(roots=mask)
total += time.perf_counter() - t0
return total / trials
def bench_invert_descent(g, root, trials):
d = g.get_dijkstra_map(root)
t0 = time.perf_counter()
for _ in range(trials):
_ = d.invert()
invert_t = (time.perf_counter() - t0) / trials
inv = d.invert()
w, h = d.root.x, d.root.y # unused, just reading
# Descent throughput: pick many start cells, step once each.
starts = [(x, y) for y in range(1, g.grid_h - 1, 10) for x in range(1, g.grid_w - 1, 10)]
n = len(starts)
t0 = time.perf_counter()
ok = 0
for _ in range(trials):
for s in starts:
if inv.descent_step(s) is not None:
ok += 1
descent_t = (time.perf_counter() - t0) / (trials * n)
return invert_t * 1000.0, descent_t * 1e6, ok // trials
def main():
rng = random.Random(SEED)
out = {"runs": []}
for (w, h) in GRID_SIZES:
g = make_grid(w, h)
for n in ROOT_COUNTS:
roots = random_points(w, h, n, rng)
ms = bench_compute(g, roots, TRIALS) * 1000.0
out["runs"].append({"grid": f"{w}x{h}", "kind": "multi_root",
"roots": n, "mean_ms": ms})
print(f" {w}x{h} multi_root({n:2d}) mean={ms:7.2f} ms")
# Mask form
mask = mcrfpy.DiscreteMap((w, h))
n_mask = max(1, int(w * h * MASK_DENSITY))
pts = random_points(w, h, n_mask, rng)
for (x, y) in pts:
mask.set(x, y, 1)
ms = bench_mask(g, mask, TRIALS) * 1000.0
out["runs"].append({"grid": f"{w}x{h}", "kind": "mask",
"roots": n_mask, "mean_ms": ms})
print(f" {w}x{h} mask({n_mask}) mean={ms:7.2f} ms")
# Invert + descent
root = (w // 2, h // 2)
inv_ms, desc_us, valid = bench_invert_descent(g, root, TRIALS)
out["runs"].append({"grid": f"{w}x{h}", "kind": "invert",
"mean_ms": inv_ms})
out["runs"].append({"grid": f"{w}x{h}", "kind": "descent_step_per_call",
"mean_us": desc_us, "valid_per_trial": valid})
print(f" {w}x{h} invert mean={inv_ms:7.2f} ms")
print(f" {w}x{h} descent_step/call mean={desc_us:7.2f} us valid={valid}")
print(json.dumps(out, indent=2))
_baseline.write("dijkstra_bench.json", out)
print("DONE")
if __name__ == "__main__":
main()
sys.exit(0)

View 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)

View file

@ -0,0 +1,92 @@
"""Benchmark: grid.step() turn manager with a mix of behaviors.
100 entities on a 100x100 grid, 1000 rounds. Mix of IDLE / NOISE4 / SEEK / FLEE.
Reports total time, mean per-round, p95 per-round.
Usage:
./mcrogueface --headless --exec ../tests/benchmarks/grid_step_bench.py
"""
import mcrfpy
import sys
import os
import time
import random
import json
sys.path.insert(0, os.path.dirname(__file__))
import _baseline
GRID_W, GRID_H = 100, 100
N_ENTITIES = 100
N_ROUNDS = 1000
SEED = 0x37
def main():
rng = random.Random(SEED)
scene = mcrfpy.Scene("bench_step")
mcrfpy.current_scene = scene
grid = mcrfpy.Grid(grid_size=(GRID_W, GRID_H))
scene.children.append(grid)
for y in range(GRID_H):
for x in range(GRID_W):
c = grid.at(x, y)
walkable = (x not in (0, GRID_W - 1)) and (y not in (0, GRID_H - 1))
c.walkable = walkable
c.transparent = walkable
# One shared threat / attractor in the center.
center = (GRID_W // 2, GRID_H // 2)
attractor = grid.get_dijkstra_map(center)
safety = attractor.invert()
for i in range(N_ENTITIES):
ex = rng.randrange(1, GRID_W - 1)
ey = rng.randrange(1, GRID_H - 1)
e = mcrfpy.Entity((ex, ey), grid=grid)
e.move_speed = 0
mix = i % 4
if mix == 0:
pass # IDLE default
elif mix == 1:
e.set_behavior(int(mcrfpy.Behavior.NOISE8))
elif mix == 2:
e.set_behavior(int(mcrfpy.Behavior.SEEK), pathfinder=attractor)
else:
e.set_behavior(int(mcrfpy.Behavior.FLEE), pathfinder=safety)
round_times = []
t_suite_start = time.perf_counter()
for _ in range(N_ROUNDS):
t0 = time.perf_counter()
grid.step()
round_times.append(time.perf_counter() - t0)
total = time.perf_counter() - t_suite_start
round_times.sort()
p95 = round_times[int(0.95 * len(round_times))]
per_step_us = (total / N_ROUNDS) / N_ENTITIES * 1e6
out = {
"grid": f"{GRID_W}x{GRID_H}",
"entities": N_ENTITIES,
"rounds": N_ROUNDS,
"total_sec": total,
"mean_round_ms": (total / N_ROUNDS) * 1000.0,
"p95_round_ms": p95 * 1000.0,
"per_entity_step_us": per_step_us,
}
print(f" total: {total:.2f} s")
print(f" mean round: {out['mean_round_ms']:.3f} ms")
print(f" p95 round: {out['p95_round_ms']:.3f} ms")
print(f" per-entity: {out['per_entity_step_us']:.2f} us")
print(json.dumps(out, indent=2))
_baseline.write("grid_step_bench.json", out)
print("DONE")
if __name__ == "__main__":
main()
sys.exit(0)

View file

@ -0,0 +1,123 @@
"""Benchmark: 1/2/4 GridView instances viewing shared Grid data (Phase 5.2).
A `mcrfpy.step()` in headless mode does not actually render -- the render path
is stubbed. To force a real render we use `automation.screenshot()`, which
flushes the current scene to a PNG via the off-screen render target. Each
screenshot is one full render of all currently-mounted children.
We measure mean wall time per screenshot for view counts {1, 2, 4} on the same
underlying grid, with grid cells populated to mimic a real overworld scene.
Usage:
./mcrogueface --headless --exec ../tests/benchmarks/gridview_render_bench.py
"""
import mcrfpy
from mcrfpy import automation
import sys
import os
import time
import json
import tempfile
sys.path.insert(0, os.path.dirname(__file__))
import _baseline
GRID_W, GRID_H = 80, 80
N_FRAMES = 60
WARMUP_FRAMES = 5
VIEW_COUNTS = [1, 2, 4]
VIEW_PIXEL_SIZE = (320, 320)
def populate_grid(g):
for y in range(GRID_H):
for x in range(GRID_W):
c = g.at(x, y)
c.walkable = True
c.transparent = True
def make_scene(view_count, tmpdir):
scene = mcrfpy.Scene(f"bench_views_{view_count}")
grid = mcrfpy.Grid(grid_size=(GRID_W, GRID_H))
populate_grid(grid)
views = []
for i in range(view_count):
# Stack views in a row; tolerate going off-screen, the renderer clips.
v = mcrfpy.GridView(
grid=grid,
pos=(i * 100, 0),
size=VIEW_PIXEL_SIZE,
)
scene.children.append(v)
views.append(v)
return scene, grid, views
def bench(view_count, tmpdir):
scene, grid, views = make_scene(view_count, tmpdir)
mcrfpy.current_scene = scene
# Warmup: a few screenshots so any first-time texture loads / shader
# compilations are amortised away.
for i in range(WARMUP_FRAMES):
automation.screenshot(os.path.join(tmpdir, f"warm_{view_count}_{i}.png"))
times = []
for i in range(N_FRAMES):
path = os.path.join(tmpdir, f"frame_{view_count}_{i}.png")
t0 = time.perf_counter()
automation.screenshot(path)
times.append(time.perf_counter() - t0)
times.sort()
total = sum(times)
mean = total / len(times)
p95 = times[int(0.95 * len(times))]
return {
"views": view_count,
"frames": N_FRAMES,
"warmup_frames": WARMUP_FRAMES,
"total_sec": total,
"mean_frame_ms": mean * 1000.0,
"p95_frame_ms": p95 * 1000.0,
"implied_fps": (1.0 / mean) if mean > 0 else float("inf"),
"per_view_frame_ms": mean * 1000.0 / view_count,
}
def main():
runs = []
with tempfile.TemporaryDirectory(prefix="mcrf_bench_") as tmpdir:
for n in VIEW_COUNTS:
r = bench(n, tmpdir)
runs.append(r)
print(f" views={r['views']} frames={r['frames']} "
f"mean={r['mean_frame_ms']:7.2f} ms "
f"p95={r['p95_frame_ms']:7.2f} ms "
f"fps~{r['implied_fps']:6.1f} "
f"per-view={r['per_view_frame_ms']:6.2f} ms")
base = runs[0]["mean_frame_ms"]
print()
for r in runs[1:]:
ratio = r["mean_frame_ms"] / base if base > 0 else 0
print(f" views={r['views']}: total frame time vs 1-view = {ratio:.2f}x")
out = {"runs": runs, "config": {
"grid": f"{GRID_W}x{GRID_H}",
"frames": N_FRAMES,
"warmup_frames": WARMUP_FRAMES,
"view_counts": VIEW_COUNTS,
"view_pixel_size": VIEW_PIXEL_SIZE,
}}
print(json.dumps(out, indent=2))
_baseline.write("gridview_render_bench.json", out)
print("DONE")
if __name__ == "__main__":
main()
sys.exit(0)

View file

@ -0,0 +1,178 @@
"""Benchmark: find_path() across grid sizes, obstacle densities, heuristics, weights,
and with/without collision-label entity blocking.
Kanban #37 coverage: pathfinding throughput at varying obstacle densities (10/30/50%)
plus an explicit with-vs-without collision-label comparison (10 / 100 entities tagged
'blocker' on a 100x100 grid).
Usage:
./mcrogueface --headless --exec ../tests/benchmarks/pathfinding_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_SIZES = [(100, 100), (500, 500)]
OBSTACLE_DENSITIES = [0.10, 0.30, 0.50]
HEURISTICS = [
("EUCLIDEAN", mcrfpy.Heuristic.EUCLIDEAN),
("MANHATTAN", mcrfpy.Heuristic.MANHATTAN),
("CHEBYSHEV", mcrfpy.Heuristic.CHEBYSHEV),
("DIAGONAL", mcrfpy.Heuristic.DIAGONAL),
("ZERO", mcrfpy.Heuristic.ZERO),
]
WEIGHTS = [1.0, 1.5, 2.0]
TRIALS_PER_CONFIG = 5
COLLIDE_GRID = (100, 100)
COLLIDE_DENSITY = 0.10
COLLIDE_BLOCKER_COUNTS = [0, 10, 100]
COLLIDE_TRIALS = 20
SEED = 0x315
def make_grid(w, h, density, seed):
rng = random.Random(seed)
g = mcrfpy.Grid(grid_size=(w, h))
for y in range(h):
for x in range(w):
c = g.at(x, y)
walkable = (x in (0, w - 1) or y in (0, h - 1)) or rng.random() > density
c.walkable = walkable
c.transparent = walkable
# Guarantee corners walkable.
g.at(1, 1).walkable = True
g.at(w - 2, h - 2).walkable = True
return g
def pick_endpoints(w, h, rng):
return (1, 1), (w - 2, h - 2)
def bench_one(g, start, end, heuristic, weight, collide, trials):
total_t = 0.0
hits = 0
length_sum = 0
for _ in range(trials):
t0 = time.perf_counter()
if collide:
p = g.find_path(start, end, heuristic=heuristic, weight=weight, collide=collide)
else:
p = g.find_path(start, end, heuristic=heuristic, weight=weight)
elapsed = time.perf_counter() - t0
total_t += elapsed
if p is not None:
steps = list(p)
if steps:
hits += 1
length_sum += len(steps)
return {
"mean_ms": (total_t / trials) * 1000.0,
"hits": hits,
"mean_length": length_sum / max(hits, 1),
}
def collide_block_section(rng):
"""100x100 grid, walkable arena, tag N entities with 'blocker' label.
Compares `find_path(..., collide='blocker')` (with) vs `find_path(...)` (without)
holding all other variables constant. The same grid is reused across N values,
walkable cells are unchanged; only the entity set differs.
"""
w, h = COLLIDE_GRID
g = make_grid(w, h, COLLIDE_DENSITY, rng.randrange(2**31))
start, end = pick_endpoints(w, h, rng)
runs = []
for n_blockers in COLLIDE_BLOCKER_COUNTS:
# Fresh entity set each iteration. Old entities are garbage-collected once
# the local list goes out of scope.
entities = []
for _ in range(n_blockers):
while True:
ex = rng.randrange(2, w - 2)
ey = rng.randrange(2, h - 2)
if g.at(ex, ey).walkable and (ex, ey) not in (start, end):
break
e = mcrfpy.Entity((ex, ey), grid=g)
e.add_label("blocker")
entities.append(e)
# WITHOUT collide arg (entities present but ignored).
wo = bench_one(g, start, end, mcrfpy.Heuristic.EUCLIDEAN, 1.0, None, COLLIDE_TRIALS)
# WITH collide arg.
wi = bench_one(g, start, end, mcrfpy.Heuristic.EUCLIDEAN, 1.0, "blocker", COLLIDE_TRIALS)
runs.append({
"grid": f"{w}x{h}",
"blockers": n_blockers,
"without_collide_ms": wo["mean_ms"],
"with_collide_ms": wi["mean_ms"],
"without_collide_path_len": wo["mean_length"],
"with_collide_path_len": wi["mean_length"],
"overhead_ms": wi["mean_ms"] - wo["mean_ms"],
})
print(f" collide n={n_blockers:<3} "
f"without={wo['mean_ms']:6.2f} ms (len={wo['mean_length']:.0f}) "
f"with={wi['mean_ms']:6.2f} ms (len={wi['mean_length']:.0f}) "
f"overhead={wi['mean_ms'] - wo['mean_ms']:+6.2f} ms")
# Drop entities from the grid before next iteration so the count is correct.
for e in entities:
e.die()
del entities
return runs
def main():
rng = random.Random(SEED)
out = {"config": {
"grid_sizes": GRID_SIZES,
"obstacle_densities": OBSTACLE_DENSITIES,
"heuristics": [h[0] for h in HEURISTICS],
"weights": WEIGHTS,
"trials": TRIALS_PER_CONFIG,
"collide_blocker_counts": COLLIDE_BLOCKER_COUNTS,
"collide_trials": COLLIDE_TRIALS,
}, "runs": []}
for (w, h) in GRID_SIZES:
for density in OBSTACLE_DENSITIES:
seed = rng.randrange(2**31)
g = make_grid(w, h, density, seed)
start, end = pick_endpoints(w, h, rng)
for (hname, heuristic) in HEURISTICS:
for weight in WEIGHTS:
r = bench_one(g, start, end, heuristic, weight, None, TRIALS_PER_CONFIG)
out["runs"].append({
"grid": f"{w}x{h}", "density": density,
"heuristic": hname, "weight": weight,
"collide": None, **r,
})
print(f" {w}x{h} d={density:.2f} h={hname:<9} w={weight:.1f} "
f"mean={r['mean_ms']:7.2f} ms len={r['mean_length']:.1f}")
print()
print(f"=== Collision-label comparison ({COLLIDE_GRID[0]}x{COLLIDE_GRID[1]}, "
f"{COLLIDE_TRIALS} trials/config) ===")
out["collide_runs"] = collide_block_section(rng)
print(json.dumps(out, indent=2))
_baseline.write("pathfinding_bench.json", out)
print("DONE")
if __name__ == "__main__":
main()
sys.exit(0)

View file

@ -0,0 +1,163 @@
"""Benchmark: spatial-hash query throughput (Phase 5.2 / card #37, scenario 3).
Per acceptance criteria: `queryRadius()` at radii (1, 5, 10, 50) with
(100, 1000, 10000) entities. The Python-facing call is `grid.entities_in_radius()`.
For each (count, radius) pair we measure:
- mean per-query time (us)
- O(n) baseline (manual scan of `grid.entities`)
- speedup factor
- mean hit count
Headless mode. Output: JSON to stdout.
Usage:
./mcrogueface --headless --exec ../tests/benchmarks/spatial_hash_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 = 200, 200
ENTITY_COUNTS = [100, 1000, 10000]
RADII = [1, 5, 10, 50]
QUERIES_PER_CONFIG = 200
SAMPLE_QUERY_LOCATIONS = 50 # how many distinct (x,y) sample positions
SEED = 0xCAFE
def build_grid(w, h):
g = mcrfpy.Grid(grid_size=(w, h))
for y in range(h):
for x in range(w):
c = g.at(x, y)
c.walkable = True
c.transparent = True
return g
def populate(g, n, rng):
ents = []
seen = set()
while len(ents) < n:
x = rng.randrange(GRID_W)
y = rng.randrange(GRID_H)
if (x, y) in seen:
continue
seen.add((x, y))
ents.append(mcrfpy.Entity((x, y), grid=g))
return ents
def sample_points(rng, n):
return [(rng.randrange(GRID_W), rng.randrange(GRID_H)) for _ in range(n)]
def bench_spatial(g, points, radius, queries):
"""Mean per-query time using SpatialHash-backed entities_in_radius."""
n_pts = len(points)
hits_total = 0
t0 = time.perf_counter()
for i in range(queries):
result = g.entities_in_radius(points[i % n_pts], radius)
hits_total += len(result)
elapsed = time.perf_counter() - t0
return elapsed / queries, hits_total / queries
def bench_naive(g, points, radius, queries):
"""O(n) baseline: enumerate grid.entities and check distance manually.
Note: `entity.x`/`entity.y` are pixel coordinates inherited from UIDrawable.
We compare against `entity.grid_pos`, which is the same coordinate frame
`entities_in_radius` uses.
"""
n_pts = len(points)
r2 = radius * radius
# Snapshot grid coordinates so the loop body has no Python attribute lookup
# cost beyond the unavoidable.
coords = [(e.grid_pos.x, e.grid_pos.y) for e in g.entities]
hits_total = 0
t0 = time.perf_counter()
for i in range(queries):
cx, cy = points[i % n_pts]
n = 0
for ex, ey in coords:
dx = ex - cx
dy = ey - cy
if dx * dx + dy * dy <= r2:
n += 1
hits_total += n
elapsed = time.perf_counter() - t0
return elapsed / queries, hits_total / queries
def main():
rng = random.Random(SEED)
runs = []
for n_ent in ENTITY_COUNTS:
scene = mcrfpy.Scene(f"spatial_{n_ent}")
mcrfpy.current_scene = scene
g = build_grid(GRID_W, GRID_H)
scene.children.append(g)
ents = populate(g, n_ent, rng)
pts = sample_points(rng, SAMPLE_QUERY_LOCATIONS)
for radius in RADII:
# Warmup the spatial hash for this entity set.
for p in pts:
g.entities_in_radius(p, radius)
sp_t, sp_hits = bench_spatial(g, pts, radius, QUERIES_PER_CONFIG)
nv_t, nv_hits = bench_naive(g, pts, radius, QUERIES_PER_CONFIG)
speedup = (nv_t / sp_t) if sp_t > 0 else float("inf")
entry = {
"entities": n_ent,
"radius": radius,
"queries": QUERIES_PER_CONFIG,
"spatial_per_query_us": sp_t * 1e6,
"naive_per_query_us": nv_t * 1e6,
"speedup": speedup,
"spatial_mean_hits": sp_hits,
"naive_mean_hits": nv_hits,
}
runs.append(entry)
print(f" n={n_ent:>5} r={radius:<3} "
f"spatial={sp_t * 1e6:9.2f} us "
f"naive={nv_t * 1e6:10.2f} us "
f"speedup={speedup:7.2f}x "
f"hits={sp_hits:6.1f} (naive={nv_hits:6.1f})")
# Tear down entities so they don't leak into the next iteration's grid.
for e in ents:
e.die()
del ents
del g
out = {
"config": {
"grid": f"{GRID_W}x{GRID_H}",
"entity_counts": ENTITY_COUNTS,
"radii": RADII,
"queries_per_config": QUERIES_PER_CONFIG,
"sample_query_locations": SAMPLE_QUERY_LOCATIONS,
"seed": SEED,
},
"runs": runs,
}
print(json.dumps(out, indent=2))
_baseline.write("spatial_hash_bench.json", out)
print("DONE")
if __name__ == "__main__":
main()
sys.exit(0)