McRogueFace/tests/benchmarks/stress_test_suite.py

344 lines
12 KiB
Python

#!/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()