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;
}
}
}
}