Tests for cached rendering performance
This commit is contained in:
parent
42fcd3417e
commit
0545dd4861
20 changed files with 1740562 additions and 0 deletions
407010
tests/benchmarks/baseline/headless_animation_stress.json
Normal file
407010
tests/benchmarks/baseline/headless_animation_stress.json
Normal file
File diff suppressed because it is too large
Load diff
194685
tests/benchmarks/baseline/headless_deep_nesting.json
Normal file
194685
tests/benchmarks/baseline/headless_deep_nesting.json
Normal file
File diff suppressed because it is too large
Load diff
677304
tests/benchmarks/baseline/headless_deep_nesting_cached.json
Normal file
677304
tests/benchmarks/baseline/headless_deep_nesting_cached.json
Normal file
File diff suppressed because it is too large
Load diff
16522
tests/benchmarks/baseline/headless_large_grid.json
Normal file
16522
tests/benchmarks/baseline/headless_large_grid.json
Normal file
File diff suppressed because it is too large
Load diff
74757
tests/benchmarks/baseline/headless_many_captions.json
Normal file
74757
tests/benchmarks/baseline/headless_many_captions.json
Normal file
File diff suppressed because it is too large
Load diff
67005
tests/benchmarks/baseline/headless_many_frames.json
Normal file
67005
tests/benchmarks/baseline/headless_many_frames.json
Normal file
File diff suppressed because it is too large
Load diff
253604
tests/benchmarks/baseline/headless_many_sprites.json
Normal file
253604
tests/benchmarks/baseline/headless_many_sprites.json
Normal file
File diff suppressed because it is too large
Load diff
18688
tests/benchmarks/baseline/headless_static_scene.json
Normal file
18688
tests/benchmarks/baseline/headless_static_scene.json
Normal file
File diff suppressed because it is too large
Load diff
14375
tests/benchmarks/baseline/headless_static_scene_cached.json
Normal file
14375
tests/benchmarks/baseline/headless_static_scene_cached.json
Normal file
File diff suppressed because it is too large
Load diff
51
tests/benchmarks/baseline/headless_summary.json
Normal file
51
tests/benchmarks/baseline/headless_summary.json
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
{
|
||||||
|
"timestamp": "2025-11-28T19:22:01.900442",
|
||||||
|
"mode": "headless",
|
||||||
|
"results": {
|
||||||
|
"many_frames": {
|
||||||
|
"avg_work_ms": 0.5644053203661328,
|
||||||
|
"max_work_ms": 1.78,
|
||||||
|
"frame_count": 3496
|
||||||
|
},
|
||||||
|
"many_sprites": {
|
||||||
|
"avg_work_ms": 0.14705301494330555,
|
||||||
|
"max_work_ms": 11.814,
|
||||||
|
"frame_count": 13317
|
||||||
|
},
|
||||||
|
"many_captions": {
|
||||||
|
"avg_work_ms": 0.49336296106557376,
|
||||||
|
"max_work_ms": 2.202,
|
||||||
|
"frame_count": 3904
|
||||||
|
},
|
||||||
|
"deep_nesting": {
|
||||||
|
"avg_work_ms": 0.3517734925606891,
|
||||||
|
"max_work_ms": 145.75,
|
||||||
|
"frame_count": 10216
|
||||||
|
},
|
||||||
|
"deep_nesting_cached": {
|
||||||
|
"avg_work_ms": 0.0942947468905298,
|
||||||
|
"max_work_ms": 100.242,
|
||||||
|
"frame_count": 35617
|
||||||
|
},
|
||||||
|
"large_grid": {
|
||||||
|
"avg_work_ms": 2.2851537544696066,
|
||||||
|
"max_work_ms": 11.534,
|
||||||
|
"frame_count": 839
|
||||||
|
},
|
||||||
|
"animation_stress": {
|
||||||
|
"avg_work_ms": 0.0924456547145996,
|
||||||
|
"max_work_ms": 11.933,
|
||||||
|
"frame_count": 21391
|
||||||
|
},
|
||||||
|
"static_scene": {
|
||||||
|
"avg_work_ms": 2.022726128016789,
|
||||||
|
"max_work_ms": 17.275,
|
||||||
|
"frame_count": 953
|
||||||
|
},
|
||||||
|
"static_scene_cached": {
|
||||||
|
"avg_work_ms": 2.694431129476584,
|
||||||
|
"max_work_ms": 22.059,
|
||||||
|
"frame_count": 726
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2291
tests/benchmarks/baseline/windowed_animation_stress.json
Normal file
2291
tests/benchmarks/baseline/windowed_animation_stress.json
Normal file
File diff suppressed because it is too large
Load diff
2291
tests/benchmarks/baseline/windowed_deep_nesting.json
Normal file
2291
tests/benchmarks/baseline/windowed_deep_nesting.json
Normal file
File diff suppressed because it is too large
Load diff
2291
tests/benchmarks/baseline/windowed_large_grid.json
Normal file
2291
tests/benchmarks/baseline/windowed_large_grid.json
Normal file
File diff suppressed because it is too large
Load diff
2291
tests/benchmarks/baseline/windowed_many_captions.json
Normal file
2291
tests/benchmarks/baseline/windowed_many_captions.json
Normal file
File diff suppressed because it is too large
Load diff
2291
tests/benchmarks/baseline/windowed_many_frames.json
Normal file
2291
tests/benchmarks/baseline/windowed_many_frames.json
Normal file
File diff suppressed because it is too large
Load diff
2291
tests/benchmarks/baseline/windowed_many_sprites.json
Normal file
2291
tests/benchmarks/baseline/windowed_many_sprites.json
Normal file
File diff suppressed because it is too large
Load diff
2291
tests/benchmarks/baseline/windowed_static_scene.json
Normal file
2291
tests/benchmarks/baseline/windowed_static_scene.json
Normal file
File diff suppressed because it is too large
Load diff
41
tests/benchmarks/baseline/windowed_summary.json
Normal file
41
tests/benchmarks/baseline/windowed_summary.json
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
{
|
||||||
|
"timestamp": "2025-11-28T16:53:30.850948",
|
||||||
|
"mode": "windowed",
|
||||||
|
"results": {
|
||||||
|
"many_frames": {
|
||||||
|
"avg_work_ms": 1.5756444444444444,
|
||||||
|
"max_work_ms": 3.257,
|
||||||
|
"frame_count": 90
|
||||||
|
},
|
||||||
|
"many_sprites": {
|
||||||
|
"avg_work_ms": 0.6889555555555555,
|
||||||
|
"max_work_ms": 1.533,
|
||||||
|
"frame_count": 90
|
||||||
|
},
|
||||||
|
"many_captions": {
|
||||||
|
"avg_work_ms": 1.2975777777777777,
|
||||||
|
"max_work_ms": 3.386,
|
||||||
|
"frame_count": 90
|
||||||
|
},
|
||||||
|
"deep_nesting": {
|
||||||
|
"avg_work_ms": 0.6173444444444445,
|
||||||
|
"max_work_ms": 1.4,
|
||||||
|
"frame_count": 90
|
||||||
|
},
|
||||||
|
"large_grid": {
|
||||||
|
"avg_work_ms": 3.6094,
|
||||||
|
"max_work_ms": 6.631,
|
||||||
|
"frame_count": 90
|
||||||
|
},
|
||||||
|
"animation_stress": {
|
||||||
|
"avg_work_ms": 0.5419333333333334,
|
||||||
|
"max_work_ms": 1.081,
|
||||||
|
"frame_count": 90
|
||||||
|
},
|
||||||
|
"static_scene": {
|
||||||
|
"avg_work_ms": 3.321588888888889,
|
||||||
|
"max_work_ms": 11.905,
|
||||||
|
"frame_count": 90
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
343
tests/benchmarks/stress_test_suite.py
Normal file
343
tests/benchmarks/stress_test_suite.py
Normal file
|
|
@ -0,0 +1,343 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Stress Test Suite for McRogueFace Performance Analysis
|
||||||
|
|
||||||
|
Establishes baseline performance data before implementing texture caching (#144).
|
||||||
|
Uses a single repeating timer pattern to avoid callback chain issues.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
./mcrogueface --headless --exec tests/benchmarks/stress_test_suite.py
|
||||||
|
"""
|
||||||
|
import mcrfpy
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
TEST_DURATION_MS = 2000
|
||||||
|
TIMER_INTERVAL_MS = 50
|
||||||
|
OUTPUT_DIR = "../tests/benchmarks/baseline"
|
||||||
|
IS_HEADLESS = True # Assume headless for automated testing
|
||||||
|
|
||||||
|
class StressTestRunner:
|
||||||
|
def __init__(self):
|
||||||
|
self.tests = []
|
||||||
|
self.current_test = -1
|
||||||
|
self.results = {}
|
||||||
|
self.frames_counted = 0
|
||||||
|
self.mode = "headless" if IS_HEADLESS else "windowed"
|
||||||
|
|
||||||
|
def add_test(self, name, setup_fn, description=""):
|
||||||
|
self.tests.append({'name': name, 'setup': setup_fn, 'description': description})
|
||||||
|
|
||||||
|
def tick(self, runtime):
|
||||||
|
"""Single timer callback that manages all test flow"""
|
||||||
|
self.frames_counted += 1
|
||||||
|
|
||||||
|
# Check if current test should end
|
||||||
|
if self.current_test >= 0 and self.frames_counted * TIMER_INTERVAL_MS >= TEST_DURATION_MS:
|
||||||
|
self.end_current_test()
|
||||||
|
self.start_next_test()
|
||||||
|
elif self.current_test < 0:
|
||||||
|
self.start_next_test()
|
||||||
|
|
||||||
|
def start_next_test(self):
|
||||||
|
self.current_test += 1
|
||||||
|
|
||||||
|
if self.current_test >= len(self.tests):
|
||||||
|
self.finish_suite()
|
||||||
|
return
|
||||||
|
|
||||||
|
test = self.tests[self.current_test]
|
||||||
|
print(f"\n[{self.current_test + 1}/{len(self.tests)}] {test['name']}")
|
||||||
|
print(f" {test['description']}")
|
||||||
|
|
||||||
|
# Setup scene
|
||||||
|
scene_name = f"stress_{self.current_test}"
|
||||||
|
mcrfpy.createScene(scene_name)
|
||||||
|
|
||||||
|
# Start benchmark
|
||||||
|
mcrfpy.start_benchmark()
|
||||||
|
mcrfpy.log_benchmark(f"TEST: {test['name']}")
|
||||||
|
|
||||||
|
# Run setup
|
||||||
|
try:
|
||||||
|
test['setup'](scene_name)
|
||||||
|
except Exception as e:
|
||||||
|
print(f" SETUP ERROR: {e}")
|
||||||
|
|
||||||
|
mcrfpy.setScene(scene_name)
|
||||||
|
self.frames_counted = 0
|
||||||
|
|
||||||
|
def end_current_test(self):
|
||||||
|
if self.current_test < 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
test = self.tests[self.current_test]
|
||||||
|
try:
|
||||||
|
filename = mcrfpy.end_benchmark()
|
||||||
|
|
||||||
|
with open(filename, 'r') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
|
||||||
|
frames = data['frames'][30:] # Skip warmup
|
||||||
|
if frames:
|
||||||
|
avg_work = sum(f['work_time_ms'] for f in frames) / len(frames)
|
||||||
|
avg_frame = sum(f['frame_time_ms'] for f in frames) / len(frames)
|
||||||
|
max_work = max(f['work_time_ms'] for f in frames)
|
||||||
|
|
||||||
|
self.results[test['name']] = {
|
||||||
|
'avg_work_ms': avg_work,
|
||||||
|
'max_work_ms': max_work,
|
||||||
|
'frame_count': len(frames),
|
||||||
|
}
|
||||||
|
print(f" Work: {avg_work:.2f}ms avg, {max_work:.2f}ms max ({len(frames)} frames)")
|
||||||
|
|
||||||
|
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
||||||
|
new_name = f"{OUTPUT_DIR}/{self.mode}_{test['name']}.json"
|
||||||
|
os.rename(filename, new_name)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ERROR: {e}")
|
||||||
|
self.results[test['name']] = {'error': str(e)}
|
||||||
|
|
||||||
|
def finish_suite(self):
|
||||||
|
mcrfpy.delTimer("tick")
|
||||||
|
|
||||||
|
print("\n" + "="*50)
|
||||||
|
print("STRESS TEST COMPLETE")
|
||||||
|
print("="*50)
|
||||||
|
|
||||||
|
for name, r in self.results.items():
|
||||||
|
if 'error' in r:
|
||||||
|
print(f" {name}: ERROR")
|
||||||
|
else:
|
||||||
|
print(f" {name}: {r['avg_work_ms']:.2f}ms avg")
|
||||||
|
|
||||||
|
# Save summary
|
||||||
|
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
||||||
|
with open(f"{OUTPUT_DIR}/{self.mode}_summary.json", 'w') as f:
|
||||||
|
json.dump({
|
||||||
|
'timestamp': datetime.now().isoformat(),
|
||||||
|
'mode': self.mode,
|
||||||
|
'results': self.results
|
||||||
|
}, f, indent=2)
|
||||||
|
|
||||||
|
print(f"\nResults saved to {OUTPUT_DIR}/")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
print("="*50)
|
||||||
|
print("McRogueFace Stress Test Suite")
|
||||||
|
print("="*50)
|
||||||
|
print(f"Tests: {len(self.tests)}, Duration: {TEST_DURATION_MS}ms each")
|
||||||
|
|
||||||
|
mcrfpy.createScene("init")
|
||||||
|
ui = mcrfpy.sceneUI("init")
|
||||||
|
ui.append(mcrfpy.Frame(pos=(0,0), size=(10,10))) # Required for timer to fire
|
||||||
|
mcrfpy.setScene("init")
|
||||||
|
mcrfpy.setTimer("tick", self.tick, TIMER_INTERVAL_MS)
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# TEST SETUP FUNCTIONS
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
def test_many_frames(scene_name):
|
||||||
|
"""1000 Frame elements"""
|
||||||
|
ui = mcrfpy.sceneUI(scene_name)
|
||||||
|
for i in range(1000):
|
||||||
|
frame = mcrfpy.Frame(
|
||||||
|
pos=((i % 32) * 32, (i // 32) * 24),
|
||||||
|
size=(30, 22),
|
||||||
|
fill_color=mcrfpy.Color((i*7)%256, (i*13)%256, (i*17)%256)
|
||||||
|
)
|
||||||
|
ui.append(frame)
|
||||||
|
mcrfpy.log_benchmark("1000 frames created")
|
||||||
|
|
||||||
|
def test_many_sprites(scene_name):
|
||||||
|
"""500 Sprite elements"""
|
||||||
|
ui = mcrfpy.sceneUI(scene_name)
|
||||||
|
texture = mcrfpy.Texture("assets/kenney_ice.png", 16, 16)
|
||||||
|
for i in range(500):
|
||||||
|
sprite = mcrfpy.Sprite(
|
||||||
|
pos=((i % 20) * 48 + 10, (i // 20) * 28 + 10),
|
||||||
|
texture=texture,
|
||||||
|
sprite_index=i % 128
|
||||||
|
)
|
||||||
|
sprite.scale_x = 2.0
|
||||||
|
sprite.scale_y = 2.0
|
||||||
|
ui.append(sprite)
|
||||||
|
mcrfpy.log_benchmark("500 sprites created")
|
||||||
|
|
||||||
|
def test_many_captions(scene_name):
|
||||||
|
"""500 Caption elements"""
|
||||||
|
ui = mcrfpy.sceneUI(scene_name)
|
||||||
|
for i in range(500):
|
||||||
|
caption = mcrfpy.Caption(
|
||||||
|
text=f"Text #{i}",
|
||||||
|
pos=((i % 20) * 50 + 5, (i // 20) * 28 + 5)
|
||||||
|
)
|
||||||
|
ui.append(caption)
|
||||||
|
mcrfpy.log_benchmark("500 captions created")
|
||||||
|
|
||||||
|
def test_deep_nesting(scene_name):
|
||||||
|
"""15-level nested frames"""
|
||||||
|
ui = mcrfpy.sceneUI(scene_name)
|
||||||
|
current = ui
|
||||||
|
for level in range(15):
|
||||||
|
frame = mcrfpy.Frame(
|
||||||
|
pos=(20, 20),
|
||||||
|
size=(1024 - level * 60, 768 - level * 45),
|
||||||
|
fill_color=mcrfpy.Color((level * 17) % 256, 100, (255 - level * 17) % 256, 200)
|
||||||
|
)
|
||||||
|
current.append(frame)
|
||||||
|
# Add children at each level
|
||||||
|
for j in range(3):
|
||||||
|
child = mcrfpy.Frame(pos=(50 + j * 80, 50), size=(60, 30))
|
||||||
|
frame.children.append(child)
|
||||||
|
current = frame.children
|
||||||
|
mcrfpy.log_benchmark("15-level nesting created")
|
||||||
|
|
||||||
|
def test_large_grid(scene_name):
|
||||||
|
"""100x100 grid with 500 entities"""
|
||||||
|
ui = mcrfpy.sceneUI(scene_name)
|
||||||
|
texture = mcrfpy.Texture("assets/kenney_ice.png", 16, 16)
|
||||||
|
grid = mcrfpy.Grid(pos=(50, 50), size=(900, 650), grid_size=(100, 100), texture=texture)
|
||||||
|
ui.append(grid)
|
||||||
|
|
||||||
|
for y in range(100):
|
||||||
|
for x in range(100):
|
||||||
|
cell = grid.at(x, y)
|
||||||
|
cell.tilesprite = (x + y) % 64
|
||||||
|
|
||||||
|
for i in range(500):
|
||||||
|
entity = mcrfpy.Entity(
|
||||||
|
grid_pos=((i * 7) % 100, (i * 11) % 100),
|
||||||
|
texture=texture,
|
||||||
|
sprite_index=(i * 3) % 128,
|
||||||
|
grid=grid
|
||||||
|
)
|
||||||
|
mcrfpy.log_benchmark("100x100 grid with 500 entities created")
|
||||||
|
|
||||||
|
def test_animation_stress(scene_name):
|
||||||
|
"""100 frames with 200 animations"""
|
||||||
|
ui = mcrfpy.sceneUI(scene_name)
|
||||||
|
for i in range(100):
|
||||||
|
frame = mcrfpy.Frame(
|
||||||
|
pos=((i % 10) * 100 + 10, (i // 10) * 70 + 10),
|
||||||
|
size=(80, 50),
|
||||||
|
fill_color=mcrfpy.Color(100, 150, 200)
|
||||||
|
)
|
||||||
|
ui.append(frame)
|
||||||
|
|
||||||
|
# Two animations per frame
|
||||||
|
anim_x = mcrfpy.Animation("x", float((i % 10) * 100 + 50), 1.5, "easeInOut")
|
||||||
|
anim_x.start(frame)
|
||||||
|
anim_o = mcrfpy.Animation("fill_color.a", 128 + (i % 128), 2.0, "linear")
|
||||||
|
anim_o.start(frame)
|
||||||
|
mcrfpy.log_benchmark("100 frames with 200 animations")
|
||||||
|
|
||||||
|
def test_static_scene(scene_name):
|
||||||
|
"""Static game scene (ideal for caching)"""
|
||||||
|
ui = mcrfpy.sceneUI(scene_name)
|
||||||
|
texture = mcrfpy.Texture("assets/kenney_ice.png", 16, 16)
|
||||||
|
|
||||||
|
# Background
|
||||||
|
bg = mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(30, 30, 50))
|
||||||
|
ui.append(bg)
|
||||||
|
|
||||||
|
# UI panel
|
||||||
|
panel = mcrfpy.Frame(pos=(10, 10), size=(200, 300), fill_color=mcrfpy.Color(50, 50, 70))
|
||||||
|
ui.append(panel)
|
||||||
|
for i in range(10):
|
||||||
|
caption = mcrfpy.Caption(text=f"Status {i}", pos=(10, 10 + i * 25))
|
||||||
|
panel.children.append(caption)
|
||||||
|
|
||||||
|
# Grid
|
||||||
|
grid = mcrfpy.Grid(pos=(220, 10), size=(790, 600), grid_size=(40, 30), texture=texture)
|
||||||
|
ui.append(grid)
|
||||||
|
for y in range(30):
|
||||||
|
for x in range(40):
|
||||||
|
grid.at(x, y).tilesprite = ((x + y) % 4) + 1
|
||||||
|
|
||||||
|
for i in range(20):
|
||||||
|
entity = mcrfpy.Entity(grid_pos=((i*2) % 40, (i*3) % 30),
|
||||||
|
texture=texture, sprite_index=64 + i % 16, grid=grid)
|
||||||
|
mcrfpy.log_benchmark("Static game scene created")
|
||||||
|
|
||||||
|
|
||||||
|
def test_static_scene_cached(scene_name):
|
||||||
|
"""Static game scene with cache_subtree enabled (#144)"""
|
||||||
|
ui = mcrfpy.sceneUI(scene_name)
|
||||||
|
texture = mcrfpy.Texture("assets/kenney_ice.png", 16, 16)
|
||||||
|
|
||||||
|
# Background with caching enabled
|
||||||
|
bg = mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(30, 30, 50), cache_subtree=True)
|
||||||
|
ui.append(bg)
|
||||||
|
|
||||||
|
# UI panel with caching enabled
|
||||||
|
panel = mcrfpy.Frame(pos=(10, 10), size=(200, 300), fill_color=mcrfpy.Color(50, 50, 70), cache_subtree=True)
|
||||||
|
ui.append(panel)
|
||||||
|
for i in range(10):
|
||||||
|
caption = mcrfpy.Caption(text=f"Status {i}", pos=(10, 10 + i * 25))
|
||||||
|
panel.children.append(caption)
|
||||||
|
|
||||||
|
# Grid (not cached - grids handle their own optimization)
|
||||||
|
grid = mcrfpy.Grid(pos=(220, 10), size=(790, 600), grid_size=(40, 30), texture=texture)
|
||||||
|
ui.append(grid)
|
||||||
|
for y in range(30):
|
||||||
|
for x in range(40):
|
||||||
|
grid.at(x, y).tilesprite = ((x + y) % 4) + 1
|
||||||
|
|
||||||
|
for i in range(20):
|
||||||
|
entity = mcrfpy.Entity(grid_pos=((i*2) % 40, (i*3) % 30),
|
||||||
|
texture=texture, sprite_index=64 + i % 16, grid=grid)
|
||||||
|
mcrfpy.log_benchmark("Static game scene with cache_subtree created")
|
||||||
|
|
||||||
|
|
||||||
|
def test_deep_nesting_cached(scene_name):
|
||||||
|
"""15-level nested frames with cache_subtree on outer frame (#144)"""
|
||||||
|
ui = mcrfpy.sceneUI(scene_name)
|
||||||
|
|
||||||
|
# Outer frame with caching - entire subtree cached
|
||||||
|
outer = mcrfpy.Frame(
|
||||||
|
pos=(0, 0),
|
||||||
|
size=(1024, 768),
|
||||||
|
fill_color=mcrfpy.Color(0, 100, 255, 200),
|
||||||
|
cache_subtree=True # Cache entire nested hierarchy
|
||||||
|
)
|
||||||
|
ui.append(outer)
|
||||||
|
|
||||||
|
current = outer.children
|
||||||
|
for level in range(15):
|
||||||
|
frame = mcrfpy.Frame(
|
||||||
|
pos=(20, 20),
|
||||||
|
size=(1024 - level * 60, 768 - level * 45),
|
||||||
|
fill_color=mcrfpy.Color((level * 17) % 256, 100, (255 - level * 17) % 256, 200)
|
||||||
|
)
|
||||||
|
current.append(frame)
|
||||||
|
# Add children at each level
|
||||||
|
for j in range(3):
|
||||||
|
child = mcrfpy.Frame(pos=(50 + j * 80, 50), size=(60, 30))
|
||||||
|
frame.children.append(child)
|
||||||
|
current = frame.children
|
||||||
|
mcrfpy.log_benchmark("15-level nesting with cache_subtree created")
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# MAIN
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
runner = StressTestRunner()
|
||||||
|
runner.add_test("many_frames", test_many_frames, "1000 Frame elements")
|
||||||
|
runner.add_test("many_sprites", test_many_sprites, "500 Sprite elements")
|
||||||
|
runner.add_test("many_captions", test_many_captions, "500 Caption elements")
|
||||||
|
runner.add_test("deep_nesting", test_deep_nesting, "15-level nested hierarchy")
|
||||||
|
runner.add_test("deep_nesting_cached", test_deep_nesting_cached, "15-level nested (cache_subtree)")
|
||||||
|
runner.add_test("large_grid", test_large_grid, "100x100 grid, 500 entities")
|
||||||
|
runner.add_test("animation_stress", test_animation_stress, "100 frames, 200 animations")
|
||||||
|
runner.add_test("static_scene", test_static_scene, "Static game scene (no caching)")
|
||||||
|
runner.add_test("static_scene_cached", test_static_scene_cached, "Static game scene (cache_subtree)")
|
||||||
|
runner.start()
|
||||||
140
tests/benchmarks/tcod_scale_test.py
Normal file
140
tests/benchmarks/tcod_scale_test.py
Normal file
|
|
@ -0,0 +1,140 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
TCOD Scaling Benchmark - Test pathfinding/FOV on large grids
|
||||||
|
|
||||||
|
Tests whether TCOD operations scale acceptably on 1000x1000 grids,
|
||||||
|
to determine if TCOD data needs chunking or can stay as single logical grid.
|
||||||
|
"""
|
||||||
|
import mcrfpy
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
# Grid sizes to test
|
||||||
|
SIZES = [(100, 100), (250, 250), (500, 500), (1000, 1000)]
|
||||||
|
ITERATIONS = 10
|
||||||
|
|
||||||
|
def benchmark_grid_size(grid_x, grid_y):
|
||||||
|
"""Benchmark TCOD operations for a given grid size"""
|
||||||
|
results = {}
|
||||||
|
|
||||||
|
# Create scene and grid
|
||||||
|
scene_name = f"bench_{grid_x}x{grid_y}"
|
||||||
|
mcrfpy.createScene(scene_name)
|
||||||
|
ui = mcrfpy.sceneUI(scene_name)
|
||||||
|
|
||||||
|
texture = mcrfpy.Texture("assets/kenney_ice.png", 16, 16)
|
||||||
|
|
||||||
|
# Time grid creation
|
||||||
|
t0 = time.perf_counter()
|
||||||
|
grid = mcrfpy.Grid(
|
||||||
|
pos=(0, 0),
|
||||||
|
size=(800, 600),
|
||||||
|
grid_size=(grid_x, grid_y),
|
||||||
|
texture=texture
|
||||||
|
)
|
||||||
|
ui.append(grid)
|
||||||
|
results['create_ms'] = (time.perf_counter() - t0) * 1000
|
||||||
|
|
||||||
|
# Set up some walkability (maze-like pattern)
|
||||||
|
t0 = time.perf_counter()
|
||||||
|
for y in range(grid_y):
|
||||||
|
for x in range(grid_x):
|
||||||
|
cell = grid.at(x, y)
|
||||||
|
# Create a simple maze: every 3rd cell is a wall
|
||||||
|
cell.walkable = not ((x % 3 == 0) and (y % 3 == 0))
|
||||||
|
cell.transparent = cell.walkable
|
||||||
|
results['setup_walkability_ms'] = (time.perf_counter() - t0) * 1000
|
||||||
|
|
||||||
|
# Add an entity for FOV perspective
|
||||||
|
entity = mcrfpy.Entity(
|
||||||
|
grid_pos=(grid_x // 2, grid_y // 2),
|
||||||
|
texture=texture,
|
||||||
|
sprite_index=64,
|
||||||
|
grid=grid
|
||||||
|
)
|
||||||
|
|
||||||
|
# Benchmark FOV computation
|
||||||
|
fov_times = []
|
||||||
|
for i in range(ITERATIONS):
|
||||||
|
# Move entity to different positions
|
||||||
|
ex, ey = (i * 7) % (grid_x - 20) + 10, (i * 11) % (grid_y - 20) + 10
|
||||||
|
t0 = time.perf_counter()
|
||||||
|
grid.compute_fov(ex, ey, radius=15)
|
||||||
|
fov_times.append((time.perf_counter() - t0) * 1000)
|
||||||
|
results['fov_avg_ms'] = sum(fov_times) / len(fov_times)
|
||||||
|
results['fov_max_ms'] = max(fov_times)
|
||||||
|
|
||||||
|
# Benchmark A* pathfinding (corner to corner)
|
||||||
|
path_times = []
|
||||||
|
for i in range(ITERATIONS):
|
||||||
|
# Path from near origin to near opposite corner
|
||||||
|
x1, y1 = 1, 1
|
||||||
|
x2, y2 = grid_x - 2, grid_y - 2
|
||||||
|
t0 = time.perf_counter()
|
||||||
|
path = grid.compute_astar_path(x1, y1, x2, y2)
|
||||||
|
path_times.append((time.perf_counter() - t0) * 1000)
|
||||||
|
results['astar_avg_ms'] = sum(path_times) / len(path_times)
|
||||||
|
results['astar_max_ms'] = max(path_times)
|
||||||
|
results['astar_path_len'] = len(path) if path else 0
|
||||||
|
|
||||||
|
# Benchmark Dijkstra (full map distance calculation)
|
||||||
|
dijkstra_times = []
|
||||||
|
for i in range(ITERATIONS):
|
||||||
|
cx, cy = grid_x // 2, grid_y // 2
|
||||||
|
t0 = time.perf_counter()
|
||||||
|
grid.compute_dijkstra(cx, cy)
|
||||||
|
dijkstra_times.append((time.perf_counter() - t0) * 1000)
|
||||||
|
results['dijkstra_avg_ms'] = sum(dijkstra_times) / len(dijkstra_times)
|
||||||
|
results['dijkstra_max_ms'] = max(dijkstra_times)
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("=" * 60)
|
||||||
|
print("TCOD Scaling Benchmark")
|
||||||
|
print("=" * 60)
|
||||||
|
print(f"Testing grid sizes: {SIZES}")
|
||||||
|
print(f"Iterations per test: {ITERATIONS}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
all_results = {}
|
||||||
|
|
||||||
|
for grid_x, grid_y in SIZES:
|
||||||
|
print(f"\n--- Grid {grid_x}x{grid_y} ({grid_x * grid_y:,} cells) ---")
|
||||||
|
try:
|
||||||
|
results = benchmark_grid_size(grid_x, grid_y)
|
||||||
|
all_results[f"{grid_x}x{grid_y}"] = results
|
||||||
|
|
||||||
|
print(f" Creation: {results['create_ms']:.2f}ms")
|
||||||
|
print(f" Walkability: {results['setup_walkability_ms']:.2f}ms")
|
||||||
|
print(f" FOV (r=15): {results['fov_avg_ms']:.3f}ms avg, {results['fov_max_ms']:.3f}ms max")
|
||||||
|
print(f" A* path: {results['astar_avg_ms']:.2f}ms avg, {results['astar_max_ms']:.2f}ms max (len={results['astar_path_len']})")
|
||||||
|
print(f" Dijkstra: {results['dijkstra_avg_ms']:.2f}ms avg, {results['dijkstra_max_ms']:.2f}ms max")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ERROR: {e}")
|
||||||
|
all_results[f"{grid_x}x{grid_y}"] = {'error': str(e)}
|
||||||
|
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print("SUMMARY - Per-frame budget analysis (targeting 16ms for 60fps)")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
for size, results in all_results.items():
|
||||||
|
if 'error' in results:
|
||||||
|
print(f" {size}: ERROR")
|
||||||
|
else:
|
||||||
|
total_logic = results['fov_avg_ms'] + results['astar_avg_ms']
|
||||||
|
print(f" {size}: FOV+A* = {total_logic:.2f}ms ({total_logic/16*100:.0f}% of frame budget)")
|
||||||
|
|
||||||
|
print("\nDone.")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
# Run immediately (no timer needed for this test)
|
||||||
|
mcrfpy.createScene("init")
|
||||||
|
mcrfpy.setScene("init")
|
||||||
|
|
||||||
|
# Use a timer to let the engine initialize
|
||||||
|
def run_benchmark(runtime):
|
||||||
|
main()
|
||||||
|
|
||||||
|
mcrfpy.setTimer("bench", run_benchmark, 100)
|
||||||
Loading…
Add table
Add a link
Reference in a new issue