using Godot; using System; using System.Collections.Generic; namespace Voider.Tools; /// /// Unified performance profiling and monitoring tool /// Combines real-time overlay, spike detection, and detailed logging /// /// Controls: /// F9 - Toggle overlay visibility /// F10 - Toggle spike logging (to console) /// F11 - Dump spike log to file /// F12 - Print performance summary /// public partial class PerformanceProfiler : CanvasLayer { #region Export Properties [Export] public bool EnableOverlay { get; set; } = false; [Export] public bool EnableSpikeLogging { get; set; } = false; [Export] public float SpikeThresholdMs { get; set; } = 20.0f; [Export] public int MaxSpikeHistory { get; set; } = 100; [Export] public int SampleWindowSize { get; set; } = 60; #endregion #region UI Elements private Label _fpsLabel; private Label _frameTimeLabel; private Label _processTimeLabel; private Label _physicsTimeLabel; private Label _memoryLabel; private Label _drawCallsLabel; private VBoxContainer _container; #endregion #region Performance Tracking private List _frameTimeSamples = new List(); private List _spikeHistory = new List(); private double _lastFrameTime; #endregion private struct FrameSpikeData { public double Timestamp; public double FrameMs; public double ProcessMs; public double PhysicsMs; public int DrawCalls; public int NodeCount; } public override void _Ready() { CreateUI(); _container.Visible = EnableOverlay; GD.Print("[PerformanceProfiler] Initialized"); GD.Print(" F9 - Toggle overlay"); GD.Print(" F10 - Toggle spike logging"); GD.Print(" F11 - Dump spike log to file"); GD.Print(" F12 - Print summary"); } private void CreateUI() { // Main container _container = new VBoxContainer { Position = new Vector2(10, 10) }; AddChild(_container); // Title var titleLabel = new Label { Text = "Performance Monitor" }; titleLabel.AddThemeFontSizeOverride("font_size", 18); _container.AddChild(titleLabel); // Separator var separator = new HSeparator(); _container.AddChild(separator); // FPS _fpsLabel = CreateLabel("FPS: --"); _container.AddChild(_fpsLabel); // Frame time _frameTimeLabel = CreateLabel("Frame: -- ms"); _container.AddChild(_frameTimeLabel); // Process time _processTimeLabel = CreateLabel("Process: -- ms"); _container.AddChild(_processTimeLabel); // Physics time _physicsTimeLabel = CreateLabel("Physics: -- ms"); _container.AddChild(_physicsTimeLabel); // Memory _memoryLabel = CreateLabel("Memory: -- MB"); _container.AddChild(_memoryLabel); // Draw calls _drawCallsLabel = CreateLabel("Draw Calls: --"); _container.AddChild(_drawCallsLabel); // Add styling var styleBox = new StyleBoxFlat { BgColor = new Color(0, 0, 0, 0.7f) }; styleBox.SetContentMarginAll(10); _container.AddThemeStyleboxOverride("panel", styleBox); } private Label CreateLabel(string text) { var label = new Label { Text = text }; label.AddThemeFontSizeOverride("font_size", 14); return label; } public override void _Process(double delta) { var frameMs = delta * 1000.0; _lastFrameTime = frameMs; // Track frame time samples _frameTimeSamples.Add(frameMs); if (_frameTimeSamples.Count > SampleWindowSize) { _frameTimeSamples.RemoveAt(0); } // Get performance metrics var processMs = Performance.GetMonitor(Performance.Monitor.TimeProcess) * 1000.0; var physicsMs = Performance.GetMonitor(Performance.Monitor.TimePhysicsProcess) * 1000.0; var drawCalls = (int)Performance.GetMonitor(Performance.Monitor.RenderTotalDrawCallsInFrame); var nodeCount = (int)Performance.GetMonitor(Performance.Monitor.ObjectNodeCount); var memoryMb = Performance.GetMonitor(Performance.Monitor.MemoryStatic) / 1_048_576.0; // Check for spikes if (EnableSpikeLogging && frameMs > SpikeThresholdMs) { LogSpike(frameMs, processMs, physicsMs, drawCalls, nodeCount); } // Update overlay if (_container.Visible) { UpdateOverlay(frameMs, processMs, physicsMs, memoryMb, drawCalls); } } private void UpdateOverlay(double frameMs, double processMs, double physicsMs, double memoryMb, int drawCalls) { var fps = 1000.0 / frameMs; _fpsLabel.Text = $"FPS: {fps:F1}"; _frameTimeLabel.Text = $"Frame: {frameMs:F2} ms"; _processTimeLabel.Text = $"Process: {processMs:F2} ms"; _physicsTimeLabel.Text = $"Physics: {physicsMs:F2} ms"; _memoryLabel.Text = $"Memory: {memoryMb:F1} MB"; _drawCallsLabel.Text = $"Draw Calls: {drawCalls}"; // Color code frame time if (frameMs > 16.67) { _frameTimeLabel.AddThemeColorOverride("font_color", Colors.Red); } else if (frameMs > 8.33) { _frameTimeLabel.AddThemeColorOverride("font_color", Colors.Yellow); } else { _frameTimeLabel.AddThemeColorOverride("font_color", Colors.Green); } } private void LogSpike(double frameMs, double processMs, double physicsMs, int drawCalls, int nodeCount) { var spike = new FrameSpikeData { Timestamp = Time.GetTicksMsec() / 1000.0, FrameMs = frameMs, ProcessMs = processMs, PhysicsMs = physicsMs, DrawCalls = drawCalls, NodeCount = nodeCount }; _spikeHistory.Add(spike); if (_spikeHistory.Count > MaxSpikeHistory) { _spikeHistory.RemoveAt(0); } GD.Print($"[SPIKE] Frame: {frameMs:F2}ms | Process: {processMs:F2}ms | Physics: {physicsMs:F2}ms | Draw Calls: {drawCalls} | Nodes: {nodeCount}"); } private void PrintSummary() { if (_frameTimeSamples.Count == 0) { GD.Print("[PerformanceProfiler] No data to summarize"); return; } double avgFrame = 0, minFrame = double.MaxValue, maxFrame = 0; foreach (var sample in _frameTimeSamples) { avgFrame += sample; if (sample < minFrame) minFrame = sample; if (sample > maxFrame) maxFrame = sample; } avgFrame /= _frameTimeSamples.Count; var avgFps = 1000.0 / avgFrame; var processMs = Performance.GetMonitor(Performance.Monitor.TimeProcess) * 1000.0; var physicsMs = Performance.GetMonitor(Performance.Monitor.TimePhysicsProcess) * 1000.0; var drawCalls = (int)Performance.GetMonitor(Performance.Monitor.RenderTotalDrawCallsInFrame); var nodeCount = (int)Performance.GetMonitor(Performance.Monitor.ObjectNodeCount); var memoryMb = Performance.GetMonitor(Performance.Monitor.MemoryStatic) / 1_048_576.0; GD.Print("========================================"); GD.Print($"[PerformanceProfiler] Summary (last {_frameTimeSamples.Count} frames):"); GD.Print($" Frame Time: Avg={avgFrame:F2}ms, Min={minFrame:F2}ms, Max={maxFrame:F2}ms"); GD.Print($" Average FPS: {avgFps:F1}"); GD.Print($" Current Process: {processMs:F2}ms"); GD.Print($" Current Physics: {physicsMs:F2}ms"); GD.Print($" Total Nodes: {nodeCount}"); GD.Print($" Draw Calls: {drawCalls}"); GD.Print($" Memory Usage: {memoryMb:F1} MB"); GD.Print($" Spikes Logged: {_spikeHistory.Count} (>{SpikeThresholdMs}ms)"); GD.Print("========================================"); } private void DumpSpikeLog() { if (_spikeHistory.Count == 0) { GD.Print("[PerformanceProfiler] No spikes to dump"); return; } var logPath = $"user://performance_spikes_{DateTime.Now:yyyyMMdd_HHmmss}.log"; using var file = Godot.FileAccess.Open(logPath, Godot.FileAccess.ModeFlags.Write); if (file == null) { GD.PrintErr($"[PerformanceProfiler] Failed to create log file: {logPath}"); return; } file.StoreLine("=== Performance Spike Log ==="); file.StoreLine($"Total Spikes: {_spikeHistory.Count}"); file.StoreLine($"Threshold: {SpikeThresholdMs}ms"); file.StoreLine($"Generated: {DateTime.Now:yyyy-MM-dd HH:mm:ss}"); file.StoreLine(""); file.StoreLine("Timestamp(s) | Frame(ms) | Process(ms) | Physics(ms) | Draw Calls | Nodes"); file.StoreLine("-----------------------------------------------------------------------------"); foreach (var spike in _spikeHistory) { file.StoreLine($"{spike.Timestamp:F2} | {spike.FrameMs:F2} | {spike.ProcessMs:F2} | {spike.PhysicsMs:F2} | {spike.DrawCalls} | {spike.NodeCount}"); } file.Close(); var absPath = ProjectSettings.GlobalizePath(logPath); GD.Print($"[PerformanceProfiler] Spike log written to: {absPath}"); GD.Print($"[PerformanceProfiler] Total spikes logged: {_spikeHistory.Count}"); } public override void _Input(InputEvent @event) { if (@event is InputEventKey keyEvent && keyEvent.Pressed) { switch (keyEvent.Keycode) { case Key.F9: _container.Visible = !_container.Visible; GD.Print($"[PerformanceProfiler] Overlay {(_container.Visible ? "SHOWN" : "HIDDEN")}"); break; case Key.F10: EnableSpikeLogging = !EnableSpikeLogging; GD.Print($"[PerformanceProfiler] Spike logging {(EnableSpikeLogging ? "ENABLED" : "DISABLED")} (>{SpikeThresholdMs}ms)"); break; case Key.F11: DumpSpikeLog(); break; case Key.F12: PrintSummary(); break; } } } }