项目文件夹

文件
2026-07-13 13:13:17 +08:00

464 行
20 KiB
C#

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
#nullable enable
using System.Diagnostics;
using System.Globalization;
using ImGuiNET;
using T3.Core.DataTypes.Vector;
using T3.Core.Stats;
using T3.Core.Utils;
using T3.Editor.Gui.Styling;
using T3.Editor.Gui.UiHelpers;
using T3.Editor.Gui.Windows.Layouts;
namespace T3.Editor.Gui.Windows.Analyze;
internal static class T3Metrics
{
public static void UiRenderingStarted()
{
_watchImgRenderTime.Restart();
_watchImgRenderTime.Start();
}
public static void UiRenderingCompleted()
{
_watchImgRenderTime.Stop();
_uiRenderDurationMs = (float)((double)_watchImgRenderTime.ElapsedTicks / Stopwatch.Frequency * 1000.0);
PerformanceMetrics.RecordUiRender(_uiRenderDurationMs);
}
public static void DrawRenderPerformanceGraph()
{
float barHeight = 4;
var offsetFromAppMenu = new Vector2(AppMenuBar.AppBarSpacingX,
(int)((ImGui.GetFrameHeight() - barHeight) * 0.5f));
var screenPosition = ImGui.GetCursorScreenPos() + offsetFromAppMenu;
float barWidth = 100;
float paddedBarWidth = barWidth + 30;
ImGui.SameLine(0, offsetFromAppMenu.X);
if (ImGui.InvisibleButton("performanceGraph", new Vector2(barWidth, ImGui.GetFrameHeight())))
{
WindowManager.ToggleInstanceVisibility<PerformanceWindow>();
}
// Glance tooltip — only when the full Performance window isn't already visible,
// so you don't get a redundant overlay on top of the window.
if (ImGui.IsItemHovered() && !WindowManager.IsAnyInstanceVisible<PerformanceWindow>())
{
CustomComponents.BeginTooltip(450 * T3Ui.UiScaleFactor);
{
ImGui.Dummy(new Vector2(250 * T3Ui.UiScaleFactor, 1));
DrawDetailedView();
ImGui.Spacing();
ImGui.PushFont(Fonts.FontSmall);
ImGui.PushStyleColor(ImGuiCol.Text, UiColors.TextMuted.Rgba);
ImGui.TextUnformatted("Click to open Performance window");
ImGui.PopStyleColor();
ImGui.PopFont();
}
CustomComponents.EndTooltip();
}
float normalFramerateLevelAt = 0.5f;
float frameTimingScaleFactor = barWidth / normalFramerateLevelAt / ExpectedFramerate;
_uiSmoothedRenderDurationMs = MathUtils.Lerp(_uiSmoothedRenderDurationMs, _uiRenderDurationMs, 0.05f);
_peakUiRenderDurationMs = _peakUiRenderDurationMs > _uiRenderDurationMs
? MathUtils.Lerp(_peakUiRenderDurationMs, _uiRenderDurationMs, 0.05f)
: _uiRenderDurationMs;
var deltaTimeMs = ImGui.GetIO().DeltaTime * 1000;
PerformanceMetrics.RecordFrame(deltaTimeMs);
_peakDeltaTimeMs = _peakDeltaTimeMs > deltaTimeMs
? MathUtils.Lerp(_peakDeltaTimeMs, deltaTimeMs, 0.05f)
: deltaTimeMs;
var drawList = ImGui.GetWindowDrawList();
// Draw Ui Render Duration
var uiTimeWidth = (float)Math.Ceiling(_uiRenderDurationMs * frameTimingScaleFactor).Clamp(0, paddedBarWidth);
drawList.AddRectFilled(screenPosition, screenPosition + new Vector2(uiTimeWidth, barHeight), ColorForUiBar);
// Draw Frame Render Duration
var deltaTimeWidth = (deltaTimeMs * frameTimingScaleFactor - uiTimeWidth).Clamp(0, paddedBarWidth);
var renderBarPos = screenPosition + new Vector2(uiTimeWidth, 0);
drawList.AddRectFilled(renderBarPos, renderBarPos + new Vector2(deltaTimeWidth, barHeight), ColorForFramerateBar);
// Draw Peak UI Duration
var peakUiTimePos = screenPosition + new Vector2((int)(_peakUiRenderDurationMs * frameTimingScaleFactor).Clamp(0, paddedBarWidth), 0);
drawList.AddRectFilled(peakUiTimePos, peakUiTimePos + new Vector2(2, barHeight), ColorForUiBar);
// Draw Peak Render Duration
var peakDeltaTimePos = screenPosition + new Vector2((int)(_peakDeltaTimeMs * frameTimingScaleFactor).Clamp(0, paddedBarWidth), 0);
drawList.AddRectFilled(peakDeltaTimePos, peakDeltaTimePos + new Vector2(2, barHeight), ColorForFramerateBar);
// Draw 60fps mark
var normalFramerateMarkerPos = screenPosition + new Vector2(ExpectedFrameDurationMs * frameTimingScaleFactor, 0);
drawList.AddRectFilled(normalFramerateMarkerPos + new Vector2(0, -1), normalFramerateMarkerPos + new Vector2(1, barHeight + 1), ColorForUiBar);
}
/// <summary>
/// Body of the Performance window: three labelled metric graphs, render summary, and per-frame render stats.
/// </summary>
internal static void DrawDetailedView()
{
DrawCopyCsvButton();
DrawLabeledGraph(PerformanceMetrics.FrameDuration, "Frame", "ms", 32f, FormatMs);
ImGui.Separator();
DrawLabeledGraph(PerformanceMetrics.UiRenderDuration, "Draw", "ms", 32f, FormatMs);
ImGui.Separator();
DrawLabeledGraph(PerformanceMetrics.GcAllocationsKb, "Mem-Alloc", "", 10_000f, FormatKb);
ImGui.TextUnformatted($"Render: {_peakDeltaTimeMs:0.0}ms");
ImGui.Spacing();
ImGui.PushFont(Fonts.FontSmall);
foreach (var (key, number) in RenderStatsCollector.ResultsForLastFrame)
{
var formattedNumber = number switch
{
> 1000000 => $"{number / 1000000.0:0.0}M",
> 1000 => $"{number / 1000.0:0.0}K",
_ => number.ToString()
};
ImGui.Text($"{formattedNumber} {key}");
}
ImGui.PopFont();
}
/// <summary>
/// Small icon button at the top of the detailed view that copies the current window's samples
/// to the clipboard as CSV. Intended for quickly sharing performance snapshots (e.g. pasting
/// into a bug report or chat).
/// </summary>
private static void DrawCopyCsvButton()
{
var contentWidth = ImGui.GetContentRegionAvail().X;
var btnSize = ImGui.GetFrameHeight();
ImGui.SetCursorPosX(ImGui.GetCursorPosX() + contentWidth - btnSize);
if (CustomComponents.TransparentIconButton(Icon.CopyToClipboard, new Vector2(btnSize)))
{
ImGui.SetClipboardText(BuildCsvExport());
}
CustomComponents.TooltipForLastItem("Copy current performance window to clipboard as CSV.");
}
/// <summary>Builds a CSV snapshot of the current performance window across all three metrics.</summary>
private static string BuildCsvExport()
{
var frameScratch = new float[PerformanceMetrics.WindowSize];
var drawScratch = new float[PerformanceMetrics.WindowSize];
var allocScratch = new float[PerformanceMetrics.WindowSize];
var frames = PerformanceMetrics.FrameDuration.AsOrderedSpan(frameScratch);
var draws = PerformanceMetrics.UiRenderDuration.AsOrderedSpan(drawScratch);
var allocs = PerformanceMetrics.GcAllocationsKb.AsOrderedSpan(allocScratch);
var count = Math.Min(Math.Min(frames.Length, draws.Length), allocs.Length);
var sb = new System.Text.StringBuilder(count * 32 + 256);
sb.Append("# TiXL Performance Export ").AppendLine(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
sb.Append("# Window: ").Append(PerformanceMetrics.WindowSize).Append(" samples Total frames: ").AppendLine(PerformanceMetrics.TotalFrameCount.ToString());
sb.AppendLine("# Columns: frame_index, frame_ms, draw_ms, alloc_kB");
sb.AppendLine("frame_index,frame_ms,draw_ms,alloc_kB");
for (var i = 0; i < count; i++)
{
sb.Append(i).Append(',')
.Append(frames[i].ToString("0.##", CultureInfo.InvariantCulture)).Append(',')
.Append(draws[i].ToString("0.##", CultureInfo.InvariantCulture)).Append(',')
.AppendLine(allocs[i].ToString("0.#", CultureInfo.InvariantCulture));
}
return sb.ToString();
}
/// <summary>
/// Renders a labelled header line plus a histogram + plot-line graph for the given metric.
/// Header has three columns: label (left) | ~average (centre) | max (right, muted).
/// The plot line auto-scales its Y-axis to the current window's max; <paramref name="domainMax"/>
/// is only used as the rightmost histogram axis tick (the fixed domain upper bound).
/// </summary>
private static void DrawLabeledGraph(RollingMetric metric, string label, string unit, float domainMax, Func<float, string> axisFormat)
{
if (metric.Count < 1)
return;
var contentWidth = ImGui.GetContentRegionAvail().X;
var startPos = ImGui.GetCursorScreenPos();
// Three-column header.
var centerText = $"~{axisFormat(metric.Average)}{unit}";
var rightText = $"{axisFormat(metric.Max)}{unit} max";
ImGui.TextUnformatted(label);
var centerSize = ImGui.CalcTextSize(centerText);
ImGui.SameLine(0, 0);
ImGui.SetCursorScreenPos(new Vector2(startPos.X + (contentWidth - centerSize.X) * 0.5f, startPos.Y));
ImGui.TextUnformatted(centerText);
var rightSize = ImGui.CalcTextSize(rightText);
ImGui.SameLine(0, 0);
ImGui.SetCursorScreenPos(new Vector2(startPos.X + contentWidth - rightSize.X, startPos.Y));
ImGui.PushStyleColor(ImGuiCol.Text, UiColors.TextMuted.Rgba);
ImGui.TextUnformatted(rightText);
ImGui.PopStyleColor();
// Graph area.
var size = new Vector2(contentWidth, 60 * T3Ui.UiScaleFactor);
var cursor = ImGui.GetCursorScreenPos();
ImGui.Dummy(size);
ImGui.Dummy(new Vector2(1, 6 * T3Ui.UiScaleFactor));
var rect = new ImRect(cursor, cursor + size);
var slots = metric.Slots;
// Axis ticks describe the histogram's fixed x-domain — same value also drives the plot Y-scale
// so the graph stays readable (a jumping auto-scaled max is harder to parse than a fixed one).
var axisLeft = axisFormat(slots[0].ValueRangeMin);
var axisMid = axisFormat(slots[slots.Length / 2].ValueRangeMin);
var axisRight = axisFormat(domainMax);
var drawList = ImGui.GetWindowDrawList();
var hoveredBucket = MetricGraphView.DrawGraph(drawList, rect, metric, _floatGraphBuffer,
BarColor, FlashColor, LineColor,
PerformanceMetrics.Now, domainMax,
axisLeft, axisMid, axisRight);
if (hoveredBucket >= 0)
DrawBucketTooltip(label, unit, axisFormat, domainMax, metric, hoveredBucket);
}
/// <summary>
/// Tooltip shown while hovering a histogram bucket. Shows count + share + estimated periodicity
/// for the current window and all-time. Holding <c>Shift</c> switches to cumulative
/// ("this bucket and everything higher") — useful for "how often do we see at least X".
/// </summary>
private static void DrawBucketTooltip(string label, string unit, Func<float, string> axisFormat,
float domainMax, RollingMetric metric, int bucketIndex)
{
var slots = metric.Slots;
var cumulative = ImGui.GetIO().KeyShift;
int countRecent;
long countTotal;
if (cumulative)
{
countRecent = 0;
countTotal = 0;
for (var i = bucketIndex; i < slots.Length; i++)
{
countRecent += slots[i].CountRecent;
countTotal += slots[i].CountTotal;
}
}
else
{
countRecent = slots[bucketIndex].CountRecent;
countTotal = slots[bucketIndex].CountTotal;
}
string rangeLabel;
if (cumulative)
{
rangeLabel = $"≥ {axisFormat(slots[bucketIndex].ValueRangeMin)}{unit}";
}
else
{
var upperEdge = bucketIndex + 1 < slots.Length
? slots[bucketIndex + 1].ValueRangeMin
: domainMax;
rangeLabel = $"{axisFormat(slots[bucketIndex].ValueRangeMin)} .. {axisFormat(upperEdge)}{unit}";
}
ImGui.BeginTooltip();
{
ImGui.PushFont(Fonts.FontBold);
ImGui.TextUnformatted(label);
ImGui.PopFont();
ImGui.PushStyleColor(ImGuiCol.Text, UiColors.TextMuted.Rgba);
ImGui.TextUnformatted($"({rangeLabel})");
ImGui.PopStyleColor();
ImGui.Separator();
// Recent window.
ImGui.TextUnformatted($"Last {PerformanceMetrics.WindowSize} Frames");
var pctRecent = countRecent / (float)PerformanceMetrics.WindowSize * 100f;
ImGui.TextUnformatted($" {countRecent}× {pctRecent:0.#}%");
// Periodicity from inner span (first..last visible occurrence) — stays steady as ticks
// scroll in and out of the window, unlike WindowSize/count which jumps in steps.
if (TryComputeInnerSpanPeriod(metric, bucketIndex, cumulative, domainMax,
out var periodFramesF, out var periodSeconds))
{
ImGui.PushStyleColor(ImGuiCol.Text, UiColors.TextMuted.Rgba);
ImGui.TextUnformatted($" ~ every {periodFramesF:0.#}F / {periodSeconds:0.00}s");
ImGui.PopStyleColor();
}
// Per-frame occurrence strip: shows *when* in the window each hit happened.
DrawOccurrenceBar(metric, bucketIndex, cumulative, domainMax);
ImGui.Spacing();
// All-time.
var total = PerformanceMetrics.TotalFrameCount;
ImGui.TextUnformatted($"Total ({total:N0} Frames)");
if (total > 0)
{
var pctTotal = countTotal / (float)total * 100f;
ImGui.TextUnformatted($" {countTotal}× {pctTotal:0.##}%");
}
ImGui.Spacing();
ImGui.PushFont(Fonts.FontSmall);
ImGui.PushStyleColor(ImGuiCol.Text, UiColors.TextMuted.Rgba);
ImGui.TextUnformatted(cumulative ? "(release Shift for single bucket)" : "(hold Shift for cumulative)");
ImGui.PopStyleColor();
ImGui.PopFont();
}
ImGui.EndTooltip();
}
/// <summary>
/// Estimates the typical period between hits in the hovered bucket(s) using the *inner span* —
/// distance between the first and last visible occurrence — instead of <c>WindowSize / count</c>.
/// This is stable as occurrences scroll in and out at the buffer edges; the naive formula jumps
/// in coarse steps and visibly flickers for short periods.
///
/// Returns false when fewer than two occurrences are present (no meaningful period available).
/// </summary>
private static bool TryComputeInnerSpanPeriod(RollingMetric metric, int bucketIndex, bool cumulative,
float domainMax, out float periodFrames, out float periodSeconds)
{
var slots = metric.Slots;
var lowerEdge = slots[bucketIndex].ValueRangeMin;
float upperEdge;
if (cumulative)
{
upperEdge = float.PositiveInfinity;
}
else
{
upperEdge = bucketIndex + 1 < slots.Length
? slots[bucketIndex + 1].ValueRangeMin
: domainMax;
}
var samples = metric.AsOrderedSpan(_floatGraphBuffer);
var firstIdx = -1;
var lastIdx = -1;
var matchCount = 0;
for (var i = 0; i < samples.Length; i++)
{
var v = samples[i];
if (v < lowerEdge || v >= upperEdge)
continue;
if (firstIdx < 0)
firstIdx = i;
lastIdx = i;
matchCount++;
}
if (matchCount < 2)
{
periodFrames = 0f;
periodSeconds = 0f;
return false;
}
periodFrames = (lastIdx - firstIdx) / (float)(matchCount - 1);
var avgFrameMs = PerformanceMetrics.FrameDuration.Average;
periodSeconds = periodFrames * avgFrameMs / 1000f;
return true;
}
/// <summary>
/// Draws a compact horizontal strip inside the tooltip where every tick marks a frame in the
/// current window whose sample fell into the hovered bucket (or any higher bucket when cumulative).
/// Makes the temporal pattern of allocations (periodic / clustered / one-off) visible at a glance.
/// Scans the value buffer directly — matching against bucket value-range bounds — so the
/// hot Metrics API stays lean.
/// </summary>
private static void DrawOccurrenceBar(RollingMetric metric, int bucketIndex, bool cumulative, float domainMax)
{
var barWidth = 220f * T3Ui.UiScaleFactor;
var barHeight = 10f * T3Ui.UiScaleFactor;
var pos = ImGui.GetCursorScreenPos();
ImGui.Dummy(new Vector2(barWidth, barHeight));
var drawList = ImGui.GetWindowDrawList();
drawList.AddRectFilled(pos, pos + new Vector2(barWidth, barHeight), UiColors.BackgroundFull.Fade(0.8f));
var samples = metric.AsOrderedSpan(_floatGraphBuffer);
var count = samples.Length;
if (count < 2)
return;
var slots = metric.Slots;
var lowerEdge = slots[bucketIndex].ValueRangeMin;
float upperEdge;
if (cumulative)
{
upperEdge = float.PositiveInfinity;
}
else
{
upperEdge = bucketIndex + 1 < slots.Length
? slots[bucketIndex + 1].ValueRangeMin
: domainMax;
}
var step = barWidth / (count - 1);
uint tickColor = UiColors.ForegroundFull;
var yTop = pos.Y + 1f;
var yBottom = pos.Y + barHeight - 1f;
for (var i = 0; i < count; i++)
{
var v = samples[i];
if (v < lowerEdge || v >= upperEdge)
continue;
var x = pos.X + i * step;
drawList.AddLine(new Vector2(x, yTop), new Vector2(x, yBottom), tickColor, 1f);
}
}
private static string FormatMs(float ms) => $"{ms:0.#}";
/// <summary>Formats a kilobyte count using K / MB / GB suffixes (e.g. 100 → "100K", 1500 → "1.5MB").</summary>
private static string FormatKb(float kb)
{
if (kb < 1000f)
return $"{kb:0}K";
if (kb < 1_000_000f)
return $"{kb / 1000f:0.#}MB";
return $"{kb / 1_000_000f:0.#}GB";
}
private static uint ColorForUiBar => UiColors.ForegroundFull.Fade(0.4f);
private static uint ColorForFramerateBar => UiColors.ForegroundFull.Fade(0.1f);
private static Color BarColor => UiColors.ForegroundFull.Fade(0.1f);
private static Color FlashColor => UiColors.ForegroundFull.Fade(0.5f);
private static Color LineColor => UiColors.ForegroundFull.Fade(0.5f);
private const float ExpectedFramerate = 60;
private const float ExpectedFrameDurationMs = 1 / ExpectedFramerate * 1000;
private static float _peakUiRenderDurationMs;
private static float _peakDeltaTimeMs;
private static float _uiRenderDurationMs;
private static float _uiSmoothedRenderDurationMs;
private static readonly Stopwatch _watchImgRenderTime = new();
// Scratch buffer for plot-line copy-out (and reused by the tooltip's occurrence strip).
// Sized to match PerformanceMetrics.WindowSize.
private static readonly float[] _floatGraphBuffer = new float[PerformanceMetrics.WindowSize];
}