307 lines
10 KiB
C#
307 lines
10 KiB
C#
using Godot;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
|
|
namespace Voider.Tools;
|
|
|
|
/// <summary>
|
|
/// 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
|
|
/// </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<double> _frameTimeSamples = new List<double>();
|
|
private List<FrameSpikeData> _spikeHistory = new List<FrameSpikeData>();
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
}
|