#!/usr/bin/env python3 """ Layer Performance Benchmark for McRogueFace (#147, #148, #123) Uses C++ benchmark logger (start_benchmark/end_benchmark) for accurate timing. Results written to JSON files for analysis. Compares rendering performance between: 1. Traditional grid.at(x,y).color API (no caching) 2. New layer system with dirty flag caching 3. Various layer configurations Usage: ./mcrogueface --exec tests/benchmarks/layer_performance_test.py # Results in benchmark_*.json files """ import mcrfpy import sys import os import json # Test configuration GRID_SIZE = 100 # 100x100 = 10,000 cells MEASURE_FRAMES = 120 WARMUP_FRAMES = 30 current_test = None frame_count = 0 test_results = {} # Store filenames for each test def run_test_phase(runtime): """Run through warmup and measurement phases.""" global frame_count frame_count += 1 if frame_count == WARMUP_FRAMES: # Start benchmark after warmup mcrfpy.start_benchmark() mcrfpy.log_benchmark(f"Test: {current_test}") elif frame_count == WARMUP_FRAMES + MEASURE_FRAMES: # End benchmark and store filename filename = mcrfpy.end_benchmark() test_results[current_test] = filename print(f" {current_test}: saved to {filename}") mcrfpy.delTimer("test_phase") run_next_test() def run_next_test(): """Run next test in sequence.""" global current_test, frame_count tests = [ ('1_base_static', setup_base_layer_static), ('2_base_modified', setup_base_layer_modified), ('3_layer_static', setup_color_layer_static), ('4_layer_modified', setup_color_layer_modified), ('5_tile_static', setup_tile_layer_static), ('6_tile_modified', setup_tile_layer_modified), ('7_multi_layer', setup_multi_layer_static), ('8_comparison', setup_base_vs_layer_comparison), ] # Find current current_idx = -1 if current_test: for i, (name, _) in enumerate(tests): if name == current_test: current_idx = i break next_idx = current_idx + 1 if next_idx >= len(tests): analyze_results() return current_test = tests[next_idx][0] frame_count = 0 print(f"\n[{next_idx + 1}/{len(tests)}] Running: {current_test}") tests[next_idx][1]() mcrfpy.setTimer("test_phase", run_test_phase, 1) # ============================================================================ # Test Scenarios # ============================================================================ def setup_base_layer_static(): """Traditional grid.at(x,y).color API - no modifications during render.""" mcrfpy.createScene("test_base_static") ui = mcrfpy.sceneUI("test_base_static") grid = mcrfpy.Grid(grid_size=(GRID_SIZE, GRID_SIZE), pos=(10, 10), size=(600, 600)) ui.append(grid) # Fill base layer using traditional API for y in range(GRID_SIZE): for x in range(GRID_SIZE): cell = grid.at(x, y) cell.color = mcrfpy.Color((x * 2) % 256, (y * 2) % 256, 128, 255) mcrfpy.setScene("test_base_static") def setup_base_layer_modified(): """Traditional API with single cell modified each frame.""" mcrfpy.createScene("test_base_mod") ui = mcrfpy.sceneUI("test_base_mod") grid = mcrfpy.Grid(grid_size=(GRID_SIZE, GRID_SIZE), pos=(10, 10), size=(600, 600)) ui.append(grid) # Fill base layer for y in range(GRID_SIZE): for x in range(GRID_SIZE): cell = grid.at(x, y) cell.color = mcrfpy.Color(100, 100, 100, 255) # Timer to modify one cell per frame mod_counter = [0] def modify_cell(runtime): x = mod_counter[0] % GRID_SIZE y = (mod_counter[0] // GRID_SIZE) % GRID_SIZE cell = grid.at(x, y) cell.color = mcrfpy.Color(255, 0, 0, 255) mod_counter[0] += 1 mcrfpy.setScene("test_base_mod") mcrfpy.setTimer("modify", modify_cell, 1) def setup_color_layer_static(): """New ColorLayer with dirty flag caching - static after fill.""" mcrfpy.createScene("test_color_static") ui = mcrfpy.sceneUI("test_color_static") grid = mcrfpy.Grid(grid_size=(GRID_SIZE, GRID_SIZE), pos=(10, 10), size=(600, 600)) ui.append(grid) # Add color layer and fill once layer = grid.add_layer("color", z_index=-1) layer.fill(mcrfpy.Color(100, 150, 200, 128)) mcrfpy.setScene("test_color_static") def setup_color_layer_modified(): """ColorLayer with single cell modified each frame - tests dirty flag.""" mcrfpy.createScene("test_color_mod") ui = mcrfpy.sceneUI("test_color_mod") grid = mcrfpy.Grid(grid_size=(GRID_SIZE, GRID_SIZE), pos=(10, 10), size=(600, 600)) ui.append(grid) layer = grid.add_layer("color", z_index=-1) layer.fill(mcrfpy.Color(100, 100, 100, 128)) # Timer to modify one cell per frame - triggers re-render mod_counter = [0] def modify_cell(runtime): x = mod_counter[0] % GRID_SIZE y = (mod_counter[0] // GRID_SIZE) % GRID_SIZE layer.set(x, y, mcrfpy.Color(255, 0, 0, 255)) mod_counter[0] += 1 mcrfpy.setScene("test_color_mod") mcrfpy.setTimer("modify", modify_cell, 1) def setup_tile_layer_static(): """TileLayer with caching - static after fill.""" mcrfpy.createScene("test_tile_static") ui = mcrfpy.sceneUI("test_tile_static") try: texture = mcrfpy.Texture("assets/kenney_ice.png", 16, 16) except: texture = None grid = mcrfpy.Grid(grid_size=(GRID_SIZE, GRID_SIZE), pos=(10, 10), size=(600, 600), texture=texture) ui.append(grid) if texture: layer = grid.add_layer("tile", z_index=-1, texture=texture) layer.fill(5) mcrfpy.setScene("test_tile_static") def setup_tile_layer_modified(): """TileLayer with single cell modified each frame.""" mcrfpy.createScene("test_tile_mod") ui = mcrfpy.sceneUI("test_tile_mod") try: texture = mcrfpy.Texture("assets/kenney_ice.png", 16, 16) except: texture = None grid = mcrfpy.Grid(grid_size=(GRID_SIZE, GRID_SIZE), pos=(10, 10), size=(600, 600), texture=texture) ui.append(grid) layer = None if texture: layer = grid.add_layer("tile", z_index=-1, texture=texture) layer.fill(5) # Timer to modify one cell per frame mod_counter = [0] def modify_cell(runtime): if layer: x = mod_counter[0] % GRID_SIZE y = (mod_counter[0] // GRID_SIZE) % GRID_SIZE layer.set(x, y, (mod_counter[0] % 20)) mod_counter[0] += 1 mcrfpy.setScene("test_tile_mod") mcrfpy.setTimer("modify", modify_cell, 1) def setup_multi_layer_static(): """Multiple layers (5 color, 5 tile) - all static.""" mcrfpy.createScene("test_multi_static") ui = mcrfpy.sceneUI("test_multi_static") try: texture = mcrfpy.Texture("assets/kenney_ice.png", 16, 16) except: texture = None grid = mcrfpy.Grid(grid_size=(GRID_SIZE, GRID_SIZE), pos=(10, 10), size=(600, 600), texture=texture) ui.append(grid) # Add 5 color layers with different z_indices and colors for i in range(5): layer = grid.add_layer("color", z_index=-(i+1)*2) layer.fill(mcrfpy.Color(50 + i*30, 100 + i*20, 150 - i*20, 50)) # Add 5 tile layers if texture: for i in range(5): layer = grid.add_layer("tile", z_index=-(i+1)*2 - 1, texture=texture) layer.fill(i * 4) print(f" Created {len(grid.layers)} layers") mcrfpy.setScene("test_multi_static") def setup_base_vs_layer_comparison(): """Direct comparison: same visual using base API vs layer API.""" mcrfpy.createScene("test_comparison") ui = mcrfpy.sceneUI("test_comparison") # Grid using ONLY the new layer system (no base layer colors) grid = mcrfpy.Grid(grid_size=(GRID_SIZE, GRID_SIZE), pos=(10, 10), size=(600, 600)) ui.append(grid) # Single color layer that covers everything layer = grid.add_layer("color", z_index=-1) # Fill with pattern (same as base_layer_static but via layer) for y in range(GRID_SIZE): for x in range(GRID_SIZE): layer.set(x, y, mcrfpy.Color((x * 2) % 256, (y * 2) % 256, 128, 255)) mcrfpy.setScene("test_comparison") # ============================================================================ # Results Analysis # ============================================================================ def analyze_results(): """Read JSON files and print comparison.""" print("\n" + "=" * 70) print("LAYER PERFORMANCE BENCHMARK RESULTS") print("=" * 70) print(f"Grid size: {GRID_SIZE}x{GRID_SIZE} = {GRID_SIZE*GRID_SIZE:,} cells") print(f"Samples per test: {MEASURE_FRAMES} frames") results = {} for test_name, filename in test_results.items(): if not os.path.exists(filename): print(f" WARNING: {filename} not found") continue with open(filename, 'r') as f: data = json.load(f) frames = data.get('frames', []) if not frames: continue # Calculate averages avg_grid = sum(f['grid_render_ms'] for f in frames) / len(frames) avg_frame = sum(f['frame_time_ms'] for f in frames) / len(frames) avg_cells = sum(f['grid_cells_rendered'] for f in frames) / len(frames) avg_work = sum(f.get('work_time_ms', 0) for f in frames) / len(frames) results[test_name] = { 'avg_grid_ms': avg_grid, 'avg_frame_ms': avg_frame, 'avg_work_ms': avg_work, 'avg_cells': avg_cells, 'samples': len(frames), } print(f"\n{'Test':<20} {'Grid (ms)':>10} {'Work (ms)':>10} {'Cells':>10}") print("-" * 70) for name in sorted(results.keys()): r = results[name] print(f"{name:<20} {r['avg_grid_ms']:>10.3f} {r['avg_work_ms']:>10.3f} {r['avg_cells']:>10.0f}") print("\n" + "-" * 70) print("ANALYSIS:") # Compare base static vs layer static if '1_base_static' in results and '3_layer_static' in results: base = results['1_base_static']['avg_grid_ms'] layer = results['3_layer_static']['avg_grid_ms'] if base > 0.001: improvement = ((base - layer) / base) * 100 print(f" Static ColorLayer vs Base: {improvement:+.1f}% " f"({'FASTER' if improvement > 0 else 'slower'})") print(f" Base: {base:.3f}ms, Layer: {layer:.3f}ms") # Compare base modified vs layer modified if '2_base_modified' in results and '4_layer_modified' in results: base = results['2_base_modified']['avg_grid_ms'] layer = results['4_layer_modified']['avg_grid_ms'] if base > 0.001: improvement = ((base - layer) / base) * 100 print(f" Modified ColorLayer vs Base: {improvement:+.1f}% " f"({'FASTER' if improvement > 0 else 'slower'})") print(f" Base: {base:.3f}ms, Layer: {layer:.3f}ms") # Cache benefit (static vs modified for layers) if '3_layer_static' in results and '4_layer_modified' in results: static = results['3_layer_static']['avg_grid_ms'] modified = results['4_layer_modified']['avg_grid_ms'] if static > 0.001: overhead = ((modified - static) / static) * 100 print(f" Layer cache hit vs miss: {overhead:+.1f}% " f"({'overhead when dirty' if overhead > 0 else 'benefit'})") print(f" Static: {static:.3f}ms, Modified: {modified:.3f}ms") print("\n" + "=" * 70) print("Benchmark JSON files saved for detailed analysis.") print("Key insight: Base layer has NO caching; layers require opt-in.") sys.exit(0) # ============================================================================ # Main # ============================================================================ if __name__ == "__main__": print("=" * 70) print("Layer Performance Benchmark (C++ timing)") print("=" * 70) print("\nThis benchmark compares:") print(" - Traditional grid.at(x,y).color API (renders every frame)") print(" - New layer system with dirty flag caching (#147, #148)") print(f"\nEach test: {WARMUP_FRAMES} warmup + {MEASURE_FRAMES} measured frames") run_next_test()