extends SceneTree ## Performance regression test — loads the full scene under a heavy matador load, ## samples per-frame times during normal play and during a mass-ragdoll stress ## spike, and asserts the frame budget holds. ## ## Run (headless): ## godot --headless --script tests/performance_test.gd ## ## Exit code 0 = within budget, 1 = a budget was exceeded (or a frame was non-finite). ## ## Budgets are deliberately generous regression guards, not a target frame rate: ## they only trip on a pathological slowdown, so the test stays stable across the ## range of machines it runs on. Every run also prints the measured numbers so a ## gradual creep is visible even when it doesn't fail. const STRESS_MATADORS := 20 const WARMUP_SEC := 1.0 const SAMPLE_FRAMES := 120 # Generous ceilings — a healthy build sits far below these. const AVG_BUDGET_MS := 16.0 # sustained cost must leave headroom for 60 fps const MAX_BUDGET_MS := 100.0 # a single hitch this large is a real regression var _passed: int = 0 var _failed: int = 0 func _init() -> void: _run.call_deferred() func _run() -> void: print("=".repeat(60)) print("PERFORMANCE TEST") print("=".repeat(60)) var dp: Node = root.get_node_or_null("/root/DP") if dp: dp.set_value("mat_spawn_count", STRESS_MATADORS) var scene_res := load("res://scene.tscn") if not scene_res: push_error("performance_test: failed to load scene.tscn") quit(1) return var scene_inst: Node = scene_res.instantiate() root.add_child(scene_inst) current_scene = scene_inst # match runtime: game code (sword throw, overlays) uses current_scene await create_timer(WARMUP_SEC).timeout var matadors := get_nodes_in_group(&"matador") print(" matadors alive: %d" % matadors.size()) var normal := await _sample_frames(SAMPLE_FRAMES) _report_phase("normal play", normal) # Stress spike: ragdoll every matador on the same frame. for mat: Node in get_nodes_in_group(&"matador"): if mat.has_method("_enter_ragdoll"): mat._enter_ragdoll(Vector3(randf() - 0.5, 0.0, randf() - 0.5).normalized(), 12.0) var stress := await _sample_frames(SAMPLE_FRAMES) _report_phase("mass ragdoll", stress) _finish() # Awaits SAMPLE_FRAMES idle frames, returning [avg_ms, max_ms, all_finite]. func _sample_frames(count: int) -> Array: var total_us := 0 var max_us := 0 var all_finite := true var last := Time.get_ticks_usec() for _i in count: await process_frame var now := Time.get_ticks_usec() var frame_us := now - last last = now total_us += frame_us max_us = maxi(max_us, frame_us) if not is_finite(float(frame_us)): all_finite = false var avg_ms := (total_us / float(count)) / 1000.0 return [avg_ms, max_us / 1000.0, all_finite] func _report_phase(label: String, sample: Array) -> void: var avg_ms: float = sample[0] var max_ms: float = sample[1] var all_finite: bool = sample[2] print("\n-- %s --" % label) print(" avg %.2f ms/frame (~%.0f fps) peak %.2f ms" % [ avg_ms, 1000.0 / maxf(avg_ms, 0.001), max_ms]) _assert_true(all_finite, "%s: all frame times finite" % label) _assert_true(avg_ms < AVG_BUDGET_MS, "%s: avg %.2f ms under %.0f ms budget" % [label, avg_ms, AVG_BUDGET_MS]) _assert_true(max_ms < MAX_BUDGET_MS, "%s: peak %.2f ms under %.0f ms budget" % [label, max_ms, MAX_BUDGET_MS]) func _assert_true(condition: bool, desc: String) -> void: if condition: _passed += 1 print(" PASS: %s" % desc) else: _failed += 1 print(" FAIL: %s" % desc) func _finish() -> void: print("") print("=".repeat(60)) print("PERFORMANCE TEST: %d passed, %d failed" % [_passed, _failed]) print("=".repeat(60)) quit(1 if _failed > 0 else 0)