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