extends Node # Per-frame profiling node for tactical-shooter. const WARMUP_FRAMES := 60 const DEFAULT_CAPTURE_FRAMES := 600 var capture_frames := DEFAULT_CAPTURE_FRAMES var frame_count := 0 var recording: Array[Dictionary] = [] var profiling_complete := false var last_tick := 0 func _ready(): # Try to read capture_frames from environment or metadata if OS.has_environment("PROFILE_FRAMES"): capture_frames = OS.get_environment("PROFILE_FRAMES").to_int() capture_frames = get_meta("capture_frames", capture_frames) print("=== Profiler Node ===") print("Capture frames: ", capture_frames) print("Warmup frames: ", WARMUP_FRAMES) print("") print("Frame,Delta(ms),GPU(ms),Physics(ms),DrawCalls,Tris,Vertices,VRAM(MB),RAM(MB),Objects") # Load the target scene from metadata or environment var target_scene = get_meta("target_scene", "") if target_scene.is_empty() and OS.has_environment("PROFILE_SCENE"): target_scene = OS.get_environment("PROFILE_SCENE") if not target_scene.is_empty(): var packed = ResourceLoader.load(target_scene) if packed: var instance = packed.instantiate() add_child(instance) print("Loaded target scene: ", target_scene) else: printerr("Failed to load target scene: ", target_scene) last_tick = Time.get_ticks_usec() print("Ready complete, process() should fire now") set_process(true) func _process(delta: float) -> void: print("_process called, frame=", frame_count) if profiling_complete: return frame_count += 1 # Calculate real delta var now := Time.get_ticks_usec() var real_delta := (now - last_tick) / 1000000.0 last_tick = now # Skip warmup frames if frame_count <= WARMUP_FRAMES: return # Check if done if frame_count > WARMUP_FRAMES + capture_frames: profiling_complete = true _finish() return var p := Performance var frame_delta := real_delta * 1000.0 var gpu_time := float(RenderingServer.get_frame_setup_time_cpu()) * 1000.0 var physics_time := p.get_monitor(p.TIME_PHYSICS_PROCESS) * 1000.0 var draw_calls := int(p.get_monitor(p.RENDER_TOTAL_DRAW_CALLS_IN_FRAME)) var tris := int(p.get_monitor(p.RENDER_TOTAL_PRIMITIVES_IN_FRAME)) var verts := int(p.get_monitor(p.RENDER_TOTAL_VERTICES_IN_FRAME)) var vram_mb := int(p.get_monitor(p.RENDER_VIDEO_MEM_USED)) / (1024 * 1024) var ram_mb := int(OS.get_static_memory_usage()) / (1024 * 1024) var objects := int(p.get_monitor(p.OBJECT_NODE_COUNT)) recording.append({ frame = frame_count - WARMUP_FRAMES, delta_ms = snapped(frame_delta, 0.01), gpu_ms = snapped(gpu_time, 0.01), physics_ms = snapped(physics_time, 0.01), draw_calls = draw_calls, tris = tris, verts = verts, vram_mb = vram_mb, ram_mb = ram_mb, objects = objects }) print("%d,%.2f,%.2f,%.2f,%d,%d,%d,%d,%d,%d" % [ frame_count - WARMUP_FRAMES, frame_delta, gpu_time, physics_time, draw_calls, tris, verts, vram_mb, ram_mb, objects ]) func _finish() -> void: print("") print("=== Profile Summary ===") if recording.is_empty(): print("No frames recorded!") get_tree().quit(1) return var deltas: Array[float] = [] for r in recording: deltas.append(r.delta_ms) deltas.sort() var total := float(recording.size()) var n := deltas.size() var p50: float = deltas[n * 50 / 100] var p95: float = deltas[n * 95 / 100] var p99: float = deltas[n * 99 / 100] var sum: float = 0.0 for d in deltas: sum += d var avg: float = sum / total var max_dc := 0 var max_tris := 0 var max_vram := 0 for r in recording: if r.draw_calls > max_dc: max_dc = r.draw_calls if r.tris > max_tris: max_tris = r.tris if r.vram_mb > max_vram: max_vram = r.vram_mb print("Frames captured: ", recording.size()) print("Avg frame time: %.2f ms" % avg) print("P50 frame time: %.2f ms" % p50) print("P95 frame time: %.2f ms" % p95) print("P99 frame time: %.2f ms" % p99) print("Max draw calls: %d" % max_dc) print("Max triangles: %d" % max_tris) print("Peak VRAM: %d MB" % max_vram) var pass_fail := "PASS" if p95 > 16.6: pass_fail = "FAIL (P95 > 16.6 ms)" elif p99 > 50.0: pass_fail = "FAIL (P99 spike > 50 ms)" print("Result: ", pass_fail) print("") print("=== Profile complete ===") get_tree().quit(0 if pass_fail == "PASS" else 1)