344 lines
12 KiB
Python
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()
|