139 lines
4.3 KiB
GDScript
139 lines
4.3 KiB
GDScript
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
|
|
|
|
# The matadors now stab the (idle) player; drop the HUD so a resulting player
|
|
# death doesn't raise the game-over screen and pause the tree mid-measurement.
|
|
var hud := _find_hud(scene_inst)
|
|
if hud != null:
|
|
hud.queue_free()
|
|
|
|
await create_timer(WARMUP_SEC).timeout
|
|
|
|
var matadors := get_nodes_in_group(&"matador")
|
|
print(" matadors alive: %d" % matadors.size())
|
|
|
|
# The mass-ragdoll below kills every matador; hold the spawner's alive count high
|
|
# so that doesn't trip the win screen (which pauses the tree) mid-measurement.
|
|
var spawners := get_nodes_in_group(&"matador_spawn")
|
|
if not spawners.is_empty():
|
|
spawners[0]._alive = 100000
|
|
|
|
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 _find_hud(n: Node) -> Node:
|
|
if n is CanvasLayer and n.has_method(&"_show_game_over"):
|
|
return n
|
|
for c: Node in n.get_children():
|
|
var r := _find_hud(c)
|
|
if r != null:
|
|
return r
|
|
return null
|
|
|
|
|
|
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)
|