You're breathtaking!

This commit is contained in:
Wojtek Figat
2020-12-07 23:40:54 +01:00
commit 6fb9eee74c
5143 changed files with 1153594 additions and 0 deletions

View File

@@ -0,0 +1,438 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System;
using FlaxEditor.GUI;
using FlaxEngine;
using FlaxEngine.GUI;
namespace FlaxEditor.Windows.Profiler
{
/// <summary>
/// The CPU performance profiling mode.
/// </summary>
/// <seealso cref="FlaxEditor.Windows.Profiler.ProfilerMode" />
internal sealed unsafe class CPU : ProfilerMode
{
private readonly SingleChart _mainChart;
private readonly Timeline _timeline;
private readonly Table _table;
private readonly SamplesBuffer<ProfilingTools.ThreadStats[]> _events = new SamplesBuffer<ProfilingTools.ThreadStats[]>();
private bool _showOnlyLastUpdateEvents;
public CPU()
: base("CPU")
{
// Layout
var panel = new Panel(ScrollBars.Vertical)
{
AnchorPreset = AnchorPresets.StretchAll,
Offsets = Margin.Zero,
Parent = this,
};
var layout = new VerticalPanel
{
AnchorPreset = AnchorPresets.HorizontalStretchTop,
Offsets = Margin.Zero,
IsScrollable = true,
Parent = panel,
};
// Chart
_mainChart = new SingleChart
{
Title = "Update",
FormatSample = v => (Mathf.RoundToInt(v * 10.0f) / 10.0f) + " ms",
Parent = layout,
};
_mainChart.SelectedSampleChanged += OnSelectedSampleChanged;
// Timeline
_timeline = new Timeline
{
Height = 340,
Parent = layout,
};
// Table
var headerColor = Style.Current.LightBackground;
_table = new Table
{
Columns = new[]
{
new ColumnDefinition
{
UseExpandCollapseMode = true,
CellAlignment = TextAlignment.Near,
Title = "Event",
TitleBackgroundColor = headerColor,
},
new ColumnDefinition
{
Title = "Total",
TitleBackgroundColor = headerColor,
FormatValue = FormatCellPercentage,
},
new ColumnDefinition
{
Title = "Self",
TitleBackgroundColor = headerColor,
FormatValue = FormatCellPercentage,
},
new ColumnDefinition
{
Title = "Time ms",
TitleBackgroundColor = headerColor,
FormatValue = FormatCellMs,
},
new ColumnDefinition
{
Title = "Self ms",
TitleBackgroundColor = headerColor,
FormatValue = FormatCellMs,
},
new ColumnDefinition
{
Title = "Memory",
TitleBackgroundColor = headerColor,
FormatValue = FormatCellBytes,
},
},
Parent = layout,
};
_table.Splits = new[]
{
0.5f,
0.1f,
0.1f,
0.1f,
0.1f,
0.1f,
};
}
private string FormatCellPercentage(object x)
{
return ((float)x).ToString("0.0") + '%';
}
private string FormatCellMs(object x)
{
return ((float)x).ToString("0.00");
}
private string FormatCellBytes(object x)
{
return Utilities.Utils.FormatBytesCount((int)x);
}
/// <inheritdoc />
public override void Clear()
{
_mainChart.Clear();
_events.Clear();
}
/// <inheritdoc />
public override void Update(ref SharedUpdateData sharedData)
{
_mainChart.AddSample(sharedData.Stats.UpdateTimeMs);
// Gather CPU events
var events = sharedData.GetEventsCPU();
_events.Add(events);
// Update timeline if using the last frame
if (_mainChart.SelectedSampleIndex == -1)
{
var viewRange = GetEventsViewRange();
UpdateTimeline(ref viewRange);
UpdateTable(ref viewRange);
}
}
/// <inheritdoc />
public override void UpdateView(int selectedFrame, bool showOnlyLastUpdateEvents)
{
_showOnlyLastUpdateEvents = showOnlyLastUpdateEvents;
_mainChart.SelectedSampleIndex = selectedFrame;
var viewRange = GetEventsViewRange();
UpdateTimeline(ref viewRange);
UpdateTable(ref viewRange);
}
private struct ViewRange
{
public double Start;
public double End;
public static ViewRange Full = new ViewRange
{
Start = float.MinValue,
End = float.MaxValue
};
public ViewRange(ref ProfilerCPU.Event e)
{
Start = e.Start - MinEventTimeMs;
End = e.End + MinEventTimeMs;
}
public bool SkipEvent(ref ProfilerCPU.Event e)
{
return e.Start < Start || e.Start > End;
}
}
private ViewRange GetEventsViewRange()
{
if (_showOnlyLastUpdateEvents)
{
// Find root event named 'Update' and use it as a view range
if (_events.Count != 0)
{
var data = _events.Get(_mainChart.SelectedSampleIndex);
if (data != null)
{
for (int j = 0; j < data.Length; j++)
{
var events = data[j].Events;
if (events == null)
continue;
for (int i = 0; i < events.Length; i++)
{
var e = events[i];
if (e.Depth == 0 && new string(e.Name) == "Update")
{
return new ViewRange(ref e);
}
}
}
}
}
}
return ViewRange.Full;
}
private void AddEvent(double startTime, int maxDepth, float xOffset, int depthOffset, int index, ProfilerCPU.Event[] events, ContainerControl parent)
{
ref ProfilerCPU.Event e = ref events[index];
double length = e.End - e.Start;
double scale = 100.0;
float x = (float)((e.Start - startTime) * scale);
float width = (float)(length * scale);
string name = new string(e.Name).Replace("::", ".");
var control = new Timeline.Event(x + xOffset, e.Depth + depthOffset, width)
{
Name = name,
TooltipText = string.Format("{0}, {1} ms", name, ((int)(length * 1000.0) / 1000.0f)),
Parent = parent,
};
// Spawn sub events
int childrenDepth = e.Depth + 1;
if (childrenDepth <= maxDepth)
{
while (++index < events.Length)
{
int subDepth = events[index].Depth;
if (subDepth <= e.Depth)
break;
if (subDepth == childrenDepth)
{
AddEvent(startTime, maxDepth, xOffset, depthOffset, index, events, parent);
}
}
}
}
private void UpdateTimeline(ref ViewRange viewRange)
{
var container = _timeline.EventsContainer;
// Clear previous events
container.DisposeChildren();
container.LockChildrenRecursive();
_timeline.Height = UpdateTimelineInner(ref viewRange);
container.UnlockChildrenRecursive();
container.PerformLayout();
}
private float UpdateTimelineInner(ref ViewRange viewRange)
{
if (_events.Count == 0)
return 0;
var data = _events.Get(_mainChart.SelectedSampleIndex);
if (data == null || data.Length == 0)
return 0;
// Find the first event start time (for the timeline start time)
double startTime = double.MaxValue;
for (int i = 0; i < data.Length; i++)
{
if (data[i].Events != null && data[i].Events.Length != 0)
startTime = Math.Min(startTime, data[i].Events[0].Start);
}
if (startTime >= double.MaxValue)
return 0;
var container = _timeline.EventsContainer;
// Create timeline track per thread
int depthOffset = 0;
for (int i = 0; i < data.Length; i++)
{
var events = data[i].Events;
if (events == null)
continue;
// Check maximum depth
int maxDepth = -1;
for (int j = 0; j < events.Length; j++)
{
var e = events[j];
// Reject events outside the view range
if (viewRange.SkipEvent(ref e))
continue;
maxDepth = Mathf.Max(maxDepth, e.Depth);
}
// Skip empty tracks
if (maxDepth == -1)
continue;
// Add thread label
float xOffset = 90;
var label = new Timeline.TrackLabel
{
Bounds = new Rectangle(0, depthOffset * Timeline.Event.DefaultHeight, xOffset, (maxDepth + 2) * Timeline.Event.DefaultHeight),
Name = data[i].Name,
BackgroundColor = Style.Current.Background * 1.1f,
Parent = container,
};
// Add events
for (int j = 0; j < events.Length; j++)
{
var e = events[j];
if (e.Depth == 0)
{
// Reject events outside the view range
if (viewRange.SkipEvent(ref e))
continue;
AddEvent(startTime, maxDepth, xOffset, depthOffset, j, events, container);
}
}
depthOffset += maxDepth + 2;
}
return Timeline.Event.DefaultHeight * depthOffset;
}
private void UpdateTable(ref ViewRange viewRange)
{
_table.DisposeChildren();
_table.LockChildrenRecursive();
UpdateTableInner(ref viewRange);
_table.UnlockChildrenRecursive();
_table.PerformLayout();
}
private void UpdateTableInner(ref ViewRange viewRange)
{
if (_events.Count == 0)
return;
var data = _events.Get(_mainChart.SelectedSampleIndex);
if (data == null || data.Length == 0)
return;
float totalTimeMs = _mainChart.SelectedSample;
// Add rows
var rowColor2 = Style.Current.Background * 1.4f;
for (int j = 0; j < data.Length; j++)
{
var events = data[j].Events;
if (events == null)
continue;
for (int i = 0; i < events.Length; i++)
{
var e = events[i];
var time = Math.Max(e.End - e.Start, MinEventTimeMs);
// Reject events outside the view range
if (viewRange.SkipEvent(ref e))
continue;
// Count sub-events time
double subEventsTimeTotal = 0;
int subEventsMemoryTotal = e.ManagedMemoryAllocation + e.NativeMemoryAllocation;
for (int k = i + 1; k < events.Length; k++)
{
var sub = events[k];
if (sub.Depth == e.Depth + 1)
{
subEventsTimeTotal += Math.Max(sub.End - sub.Start, MinEventTimeMs);
}
else if (sub.Depth <= e.Depth)
{
break;
}
subEventsMemoryTotal += sub.ManagedMemoryAllocation + e.NativeMemoryAllocation;
}
string name = new string(e.Name).Replace("::", ".");
var row = new Row
{
Values = new object[]
{
// Event
name,
// Total (%)
(int)(time / totalTimeMs * 1000.0f) / 10.0f,
// Self (%)
(int)((time - subEventsTimeTotal) / time * 1000.0f) / 10.0f,
// Time ms
(float)((time * 10000.0f) / 10000.0f),
// Self ms
(float)(((time - subEventsTimeTotal) * 10000.0f) / 10000.0f),
// Memory Alloc
subEventsMemoryTotal,
},
Depth = e.Depth,
Width = _table.Width,
Parent = _table,
};
if (i % 2 == 0)
row.BackgroundColor = rowColor2;
row.Visible = e.Depth < 3;
}
}
}
}
}

View File

@@ -0,0 +1,315 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using FlaxEditor.GUI;
using FlaxEngine;
using FlaxEngine.GUI;
namespace FlaxEditor.Windows.Profiler
{
/// <summary>
/// The GPU performance profiling mode.
/// </summary>
/// <seealso cref="FlaxEditor.Windows.Profiler.ProfilerMode" />
internal sealed unsafe class GPU : ProfilerMode
{
private readonly SingleChart _drawTimeCPU;
private readonly SingleChart _drawTimeGPU;
private readonly Timeline _timeline;
private readonly Table _table;
private readonly SamplesBuffer<ProfilerGPU.Event[]> _events = new SamplesBuffer<ProfilerGPU.Event[]>();
public GPU()
: base("GPU")
{
// Layout
var panel = new Panel(ScrollBars.Vertical)
{
AnchorPreset = AnchorPresets.StretchAll,
Offsets = Margin.Zero,
Parent = this,
};
var layout = new VerticalPanel
{
AnchorPreset = AnchorPresets.HorizontalStretchTop,
Offsets = Margin.Zero,
IsScrollable = true,
Parent = panel,
};
// Chart
_drawTimeCPU = new SingleChart
{
Title = "Draw (CPU)",
FormatSample = v => (Mathf.RoundToInt(v * 10.0f) / 10.0f) + " ms",
Parent = layout,
};
_drawTimeCPU.SelectedSampleChanged += OnSelectedSampleChanged;
_drawTimeGPU = new SingleChart
{
Title = "Draw (GPU)",
FormatSample = v => (Mathf.RoundToInt(v * 10.0f) / 10.0f) + " ms",
Parent = layout,
};
_drawTimeGPU.SelectedSampleChanged += OnSelectedSampleChanged;
// Timeline
_timeline = new Timeline
{
Height = 340,
Parent = layout,
};
// Table
var headerColor = Style.Current.LightBackground;
_table = new Table
{
Columns = new[]
{
new ColumnDefinition
{
UseExpandCollapseMode = true,
CellAlignment = TextAlignment.Near,
Title = "Event",
TitleBackgroundColor = headerColor,
},
new ColumnDefinition
{
Title = "Total",
TitleBackgroundColor = headerColor,
FormatValue = (x) => ((float)x).ToString("0.0") + '%',
},
new ColumnDefinition
{
Title = "GPU ms",
TitleBackgroundColor = headerColor,
FormatValue = (x) => ((float)x).ToString("0.000"),
},
new ColumnDefinition
{
Title = "Draw Calls",
TitleBackgroundColor = headerColor,
},
new ColumnDefinition
{
Title = "Triangles",
TitleBackgroundColor = headerColor,
},
new ColumnDefinition
{
Title = "Vertices",
TitleBackgroundColor = headerColor,
},
},
Parent = layout,
};
_table.Splits = new[]
{
0.5f,
0.1f,
0.1f,
0.1f,
0.1f,
0.1f,
};
}
/// <inheritdoc />
public override void Clear()
{
_drawTimeCPU.Clear();
_drawTimeGPU.Clear();
_events.Clear();
}
/// <inheritdoc />
public override void Update(ref SharedUpdateData sharedData)
{
// Gather GPU events
var data = sharedData.GetEventsGPU();
_events.Add(data);
// Peek draw time
_drawTimeCPU.AddSample(sharedData.Stats.DrawCPUTimeMs);
_drawTimeGPU.AddSample(sharedData.Stats.DrawGPUTimeMs);
// Update timeline if using the last frame
if (_drawTimeCPU.SelectedSampleIndex == -1)
{
UpdateTimeline();
UpdateTable();
}
}
/// <inheritdoc />
public override void UpdateView(int selectedFrame, bool showOnlyLastUpdateEvents)
{
_drawTimeCPU.SelectedSampleIndex = selectedFrame;
_drawTimeGPU.SelectedSampleIndex = selectedFrame;
UpdateTimeline();
UpdateTable();
}
private float AddEvent(float x, int maxDepth, int index, ProfilerGPU.Event[] events, ContainerControl parent)
{
ref ProfilerGPU.Event e = ref events[index];
double scale = 100.0;
float width = (float)(e.Time * scale);
string name = new string(e.Name);
var control = new Timeline.Event(x, e.Depth, width)
{
Name = name,
TooltipText = string.Format("{0}, {1} ms", name, ((int)(e.Time * 10000.0) / 10000.0f)),
Parent = parent,
};
// Spawn sub events
int childrenDepth = e.Depth + 1;
if (childrenDepth <= maxDepth)
{
// Count sub events total duration
double subEventsDuration = 0;
int tmpIndex = index;
while (++tmpIndex < events.Length)
{
int subDepth = events[tmpIndex].Depth;
if (subDepth <= e.Depth)
break;
if (subDepth == childrenDepth)
subEventsDuration += events[tmpIndex].Time;
}
// Skip if has no sub events
if (subEventsDuration > 0)
{
// Apply some offset to sub-events (center them within this event)
x += (float)((e.Time - subEventsDuration) * scale) * 0.5f;
while (++index < events.Length)
{
int subDepth = events[index].Depth;
if (subDepth <= e.Depth)
break;
if (subDepth == childrenDepth)
{
x += AddEvent(x, maxDepth, index, events, parent);
}
}
}
}
return width;
}
private void UpdateTimeline()
{
var container = _timeline.EventsContainer;
// Clear previous events
container.DisposeChildren();
container.LockChildrenRecursive();
_timeline.Height = UpdateTimelineInner();
container.UnlockChildrenRecursive();
container.PerformLayout();
}
private float UpdateTimelineInner()
{
if (_events.Count == 0)
return 0;
var data = _events.Get(_drawTimeCPU.SelectedSampleIndex);
if (data == null || data.Length == 0)
return 0;
var container = _timeline.EventsContainer;
var events = data;
// Check maximum depth
int maxDepth = 0;
for (int j = 0; j < events.Length; j++)
{
maxDepth = Mathf.Max(maxDepth, events[j].Depth);
}
// Add events
float x = 0;
for (int j = 0; j < events.Length; j++)
{
if (events[j].Depth == 0)
{
x += AddEvent(x, maxDepth, j, events, container);
}
}
return Timeline.Event.DefaultHeight * (maxDepth + 2);
}
private void UpdateTable()
{
_table.DisposeChildren();
_table.LockChildrenRecursive();
UpdateTableInner();
_table.UnlockChildrenRecursive();
_table.PerformLayout();
}
private void UpdateTableInner()
{
if (_events.Count == 0)
return;
var data = _events.Get(_drawTimeCPU.SelectedSampleIndex);
if (data == null || data.Length == 0)
return;
float totalTimeMs = _drawTimeCPU.SelectedSample;
// Add rows
var rowColor2 = Style.Current.Background * 1.4f;
for (int i = 0; i < data.Length; i++)
{
var e = data[i];
string name = new string(e.Name);
var row = new Row
{
Values = new object[]
{
// Event
name,
// Total (%)
(int)(e.Time / totalTimeMs * 1000.0f) / 10.0f,
// GPU ms
(e.Time * 10000.0f) / 10000.0f,
// Draw Calls
e.Stats.DrawCalls,
// Triangles
e.Stats.Triangles,
// Vertices
e.Stats.Vertices,
},
Depth = e.Depth,
Width = _table.Width,
Parent = _table,
};
if (i % 2 == 0)
row.BackgroundColor = rowColor2;
row.Visible = e.Depth < 3;
}
}
}
}

View File

@@ -0,0 +1,91 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System;
using FlaxEngine.GUI;
namespace FlaxEditor.Windows.Profiler
{
/// <summary>
/// The memory profiling mode focused on system memory allocations breakdown.
/// </summary>
/// <seealso cref="FlaxEditor.Windows.Profiler.ProfilerMode" />
internal sealed class Memory : ProfilerMode
{
private readonly SingleChart _nativeAllocationsChart;
private readonly SingleChart _managedAllocationsChart;
public Memory()
: base("Memory")
{
// Layout
var panel = new Panel(ScrollBars.Vertical)
{
AnchorPreset = AnchorPresets.StretchAll,
Offsets = Margin.Zero,
Parent = this,
};
var layout = new VerticalPanel
{
AnchorPreset = AnchorPresets.HorizontalStretchTop,
Offsets = Margin.Zero,
IsScrollable = true,
Parent = panel,
};
// Chart
_nativeAllocationsChart = new SingleChart
{
Title = "Native Memory Allocation",
FormatSample = v => Utilities.Utils.FormatBytesCount((int)v),
Parent = layout,
};
_nativeAllocationsChart.SelectedSampleChanged += OnSelectedSampleChanged;
_managedAllocationsChart = new SingleChart
{
Title = "Managed Memory Allocation",
FormatSample = v => Utilities.Utils.FormatBytesCount((int)v),
Parent = layout,
};
_managedAllocationsChart.SelectedSampleChanged += OnSelectedSampleChanged;
}
/// <inheritdoc />
public override void Clear()
{
_nativeAllocationsChart.Clear();
_managedAllocationsChart.Clear();
}
/// <inheritdoc />
public override void Update(ref SharedUpdateData sharedData)
{
// Count memory allocated during last frame
int nativeMemoryAllocation = 0;
int managedMemoryAllocation = 0;
var events = sharedData.GetEventsCPU();
var length = events?.Length ?? 0;
for (int i = 0; i < length; i++)
{
var ee = events[i].Events;
if (ee == null)
continue;
for (int j = 0; j < ee.Length; j++)
{
ref var e = ref ee[j];
nativeMemoryAllocation += e.NativeMemoryAllocation;
managedMemoryAllocation += e.ManagedMemoryAllocation;
}
}
_nativeAllocationsChart.AddSample(nativeMemoryAllocation);
_managedAllocationsChart.AddSample(managedMemoryAllocation);
}
/// <inheritdoc />
public override void UpdateView(int selectedFrame, bool showOnlyLastUpdateEvents)
{
_nativeAllocationsChart.SelectedSampleIndex = selectedFrame;
_managedAllocationsChart.SelectedSampleIndex = selectedFrame;
}
}
}

View File

@@ -0,0 +1,116 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using FlaxEngine;
using FlaxEngine.GUI;
namespace FlaxEditor.Windows.Profiler
{
/// <summary>
/// The general profiling mode with major game performance charts and stats.
/// </summary>
/// <seealso cref="FlaxEditor.Windows.Profiler.ProfilerMode" />
internal sealed class Overall : ProfilerMode
{
private readonly SingleChart _fpsChart;
private readonly SingleChart _updateTimeChart;
private readonly SingleChart _drawTimeCPUChart;
private readonly SingleChart _drawTimeGPUChart;
private readonly SingleChart _cpuMemChart;
private readonly SingleChart _gpuMemChart;
public Overall()
: base("Overall")
{
// Layout
var panel = new Panel(ScrollBars.Vertical)
{
AnchorPreset = AnchorPresets.StretchAll,
Offsets = Margin.Zero,
Parent = this,
};
var layout = new VerticalPanel
{
AnchorPreset = AnchorPresets.HorizontalStretchTop,
Offsets = Margin.Zero,
IsScrollable = true,
Parent = panel,
};
// Charts
_fpsChart = new SingleChart
{
Title = "FPS",
Parent = layout,
};
_fpsChart.SelectedSampleChanged += OnSelectedSampleChanged;
_updateTimeChart = new SingleChart
{
Title = "Update Time",
FormatSample = v => (Mathf.RoundToInt(v * 10.0f) / 10.0f) + " ms",
Parent = layout,
};
_updateTimeChart.SelectedSampleChanged += OnSelectedSampleChanged;
_drawTimeCPUChart = new SingleChart
{
Title = "Draw Time (CPU)",
FormatSample = v => (Mathf.RoundToInt(v * 10.0f) / 10.0f) + " ms",
Parent = layout,
};
_drawTimeCPUChart.SelectedSampleChanged += OnSelectedSampleChanged;
_drawTimeGPUChart = new SingleChart
{
Title = "Draw Time (GPU)",
FormatSample = v => (Mathf.RoundToInt(v * 10.0f) / 10.0f) + " ms",
Parent = layout,
};
_drawTimeGPUChart.SelectedSampleChanged += OnSelectedSampleChanged;
_cpuMemChart = new SingleChart
{
Title = "CPU Memory",
FormatSample = v => ((int)v) + " MB",
Parent = layout,
};
_cpuMemChart.SelectedSampleChanged += OnSelectedSampleChanged;
_gpuMemChart = new SingleChart
{
Title = "GPU Memory",
FormatSample = v => ((int)v) + " MB",
Parent = layout,
};
_gpuMemChart.SelectedSampleChanged += OnSelectedSampleChanged;
}
/// <inheritdoc />
public override void Clear()
{
_fpsChart.Clear();
_updateTimeChart.Clear();
_drawTimeCPUChart.Clear();
_drawTimeGPUChart.Clear();
_cpuMemChart.Clear();
_gpuMemChart.Clear();
}
/// <inheritdoc />
public override void Update(ref SharedUpdateData sharedData)
{
_fpsChart.AddSample(sharedData.Stats.FPS);
_updateTimeChart.AddSample(sharedData.Stats.UpdateTimeMs);
_drawTimeCPUChart.AddSample(sharedData.Stats.DrawCPUTimeMs);
_drawTimeGPUChart.AddSample(sharedData.Stats.DrawGPUTimeMs);
_cpuMemChart.AddSample(sharedData.Stats.ProcessMemory.UsedPhysicalMemory / 1024 / 1024);
_gpuMemChart.AddSample(sharedData.Stats.MemoryGPU.Used / 1024 / 1024);
}
/// <inheritdoc />
public override void UpdateView(int selectedFrame, bool showOnlyLastUpdateEvents)
{
_fpsChart.SelectedSampleIndex = selectedFrame;
_updateTimeChart.SelectedSampleIndex = selectedFrame;
_drawTimeCPUChart.SelectedSampleIndex = selectedFrame;
_drawTimeGPUChart.SelectedSampleIndex = selectedFrame;
_cpuMemChart.SelectedSampleIndex = selectedFrame;
_gpuMemChart.SelectedSampleIndex = selectedFrame;
}
}
}

View File

@@ -0,0 +1,126 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System;
using FlaxEditor.GUI.Tabs;
using FlaxEngine;
namespace FlaxEditor.Windows.Profiler
{
/// <summary>
/// Base class for all profiler modes. Implementation collects profiling events and presents it using dedicated UI.
/// </summary>
public class ProfilerMode : Tab
{
/// <summary>
/// The shared data container for the profiler modes. Used to reduce calls to profiler tool backend for the same data across different profiler fronted modes.
/// </summary>
public struct SharedUpdateData
{
private ProfilingTools.ThreadStats[] _cpuEvents;
private ProfilerGPU.Event[] _gpuEvents;
/// <summary>
/// The main stats. Gathered by auto by profiler before profiler mode update.
/// </summary>
public ProfilingTools.MainStats Stats;
/// <summary>
/// Gets the collected CPU events by the profiler from local or remote session.
/// </summary>
/// <returns>Buffer with events per thread.</returns>
public ProfilingTools.ThreadStats[] GetEventsCPU()
{
return _cpuEvents ?? (_cpuEvents = ProfilingTools.EventsCPU);
}
/// <summary>
/// Gets the collected GPU events by the profiler from local or remote session.
/// </summary>
/// <returns>Buffer with rendering events.</returns>
public ProfilerGPU.Event[] GetEventsGPU()
{
return _gpuEvents ?? (_gpuEvents = ProfilingTools.EventsGPU);
}
/// <summary>
/// Begins the data usage. Prepares the container.
/// </summary>
public void Begin()
{
Stats = ProfilingTools.Stats;
_cpuEvents = null;
_gpuEvents = null;
}
/// <summary>
/// Ends the data usage. Cleanups the container.
/// </summary>
public void End()
{
_cpuEvents = null;
_gpuEvents = null;
}
}
/// <summary>
/// The maximum amount of samples to collect.
/// </summary>
public const int MaxSamples = 60 * 5;
/// <summary>
/// The minimum event time in ms.
/// </summary>
public const double MinEventTimeMs = 0.000000001;
/// <summary>
/// Occurs when selected sample gets changed. Profiling window should propagate this change to all charts and view modes.
/// </summary>
public event Action<int> SelectedSampleChanged;
/// <inheritdoc />
public ProfilerMode(string text)
: base(text)
{
}
/// <summary>
/// Initializes this instance.
/// </summary>
public virtual void Init()
{
}
/// <summary>
/// Clears this instance.
/// </summary>
public virtual void Clear()
{
}
/// <summary>
/// Updates this instance. Called every frame if live recording is enabled.
/// </summary>
/// <param name="sharedData">The shared data.</param>
public virtual void Update(ref SharedUpdateData sharedData)
{
}
/// <summary>
/// Updates the mode view. Called after init and on selected frame changed.
/// </summary>
/// <param name="selectedFrame">The selected frame index.</param>
/// <param name="showOnlyLastUpdateEvents">True if show only events that happened during the last engine update (excluding events from fixed update or draw event), otherwise show all collected events.</param>
public virtual void UpdateView(int selectedFrame, bool showOnlyLastUpdateEvents)
{
}
/// <summary>
/// Called when selected sample gets changed.
/// </summary>
/// <param name="frameIndex">Index of the view frame.</param>
protected virtual void OnSelectedSampleChanged(int frameIndex)
{
SelectedSampleChanged?.Invoke(frameIndex);
}
}
}

View File

@@ -0,0 +1,240 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System;
using FlaxEditor.GUI;
using FlaxEditor.GUI.Tabs;
using FlaxEngine;
using FlaxEngine.GUI;
namespace FlaxEditor.Windows.Profiler
{
/// <summary>
/// Editor tool window for profiling games.
/// </summary>
/// <seealso cref="FlaxEditor.Windows.EditorWindow" />
public sealed class ProfilerWindow : EditorWindow
{
private readonly ToolStripButton _liveRecordingButton;
private readonly ToolStripButton _clearButton;
private readonly ToolStripButton _prevFrameButton;
private readonly ToolStripButton _nextFrameButton;
private readonly ToolStripButton _lastFrameButton;
private readonly ToolStripButton _showOnlyLastUpdateEventsButton;
private readonly Tabs _tabs;
private int _frameIndex = -1;
private int _framesCount;
private bool _showOnlyLastUpdateEvents = true;
/// <summary>
/// Gets or sets a value indicating whether live events recording is enabled.
/// </summary>
public bool LiveRecording
{
get => _liveRecordingButton.Checked;
set
{
if (value != LiveRecording)
{
_liveRecordingButton.Checked = value;
}
}
}
/// <summary>
/// Gets or sets the index of the selected frame to view (note: some view modes may not use it).
/// </summary>
public int ViewFrameIndex
{
get => _frameIndex;
set
{
value = Mathf.Clamp(value, -1, _framesCount - 1);
if (_frameIndex != value)
{
_frameIndex = value;
UpdateButtons();
if (_tabs.SelectedTab is ProfilerMode mode)
mode.UpdateView(_frameIndex, _showOnlyLastUpdateEvents);
}
}
}
/// <summary>
/// Gets or sets a value indicating whether show only last update events and hide events from the other callbacks (e.g. draw or fixed update).
/// </summary>
public bool ShowOnlyLastUpdateEvents
{
get => _showOnlyLastUpdateEvents;
set
{
if (_showOnlyLastUpdateEvents != value)
{
_showOnlyLastUpdateEvents = value;
UpdateButtons();
if (_tabs.SelectedTab is ProfilerMode mode)
mode.UpdateView(_frameIndex, _showOnlyLastUpdateEvents);
}
}
}
/// <summary>
/// Initializes a new instance of the <see cref="ProfilerWindow"/> class.
/// </summary>
/// <param name="editor">The editor.</param>
public ProfilerWindow(Editor editor)
: base(editor, true, ScrollBars.None)
{
Title = "Profiler";
var toolstrip = new ToolStrip
{
Offsets = new Margin(0, 0, 0, 32),
Parent = this,
};
_liveRecordingButton = toolstrip.AddButton(editor.Icons.Play32);
_liveRecordingButton.LinkTooltip("Live profiling events recording");
_liveRecordingButton.AutoCheck = true;
_clearButton = toolstrip.AddButton(editor.Icons.Rotate32, Clear);
_clearButton.LinkTooltip("Clear data");
toolstrip.AddSeparator();
_prevFrameButton = toolstrip.AddButton(editor.Icons.ArrowLeft32, () => ViewFrameIndex--);
_prevFrameButton.LinkTooltip("Previous frame");
_nextFrameButton = toolstrip.AddButton(editor.Icons.ArrowRight32, () => ViewFrameIndex++);
_nextFrameButton.LinkTooltip("Next frame");
_lastFrameButton = toolstrip.AddButton(editor.Icons.Step32, () => ViewFrameIndex = -1);
_lastFrameButton.LinkTooltip("Current frame");
toolstrip.AddSeparator();
_showOnlyLastUpdateEventsButton = toolstrip.AddButton(editor.Icons.PageScale32, () => ShowOnlyLastUpdateEvents = !ShowOnlyLastUpdateEvents);
_showOnlyLastUpdateEventsButton.LinkTooltip("Show only last update events and hide events from the other callbacks (e.g. draw or fixed update)");
_tabs = new Tabs
{
Orientation = Orientation.Vertical,
AnchorPreset = AnchorPresets.StretchAll,
Offsets = new Margin(0, 0, toolstrip.Bottom, 0),
TabsSize = new Vector2(120, 32),
Parent = this
};
_tabs.SelectedTabChanged += OnSelectedTabChanged;
}
/// <summary>
/// Adds the mode.
/// </summary>
/// <remarks>
/// To remove the mode simply call <see cref="Control.Dispose"/> on mode.
/// </remarks>
/// <param name="mode">The mode.</param>
public void AddMode(ProfilerMode mode)
{
if (mode == null)
throw new ArgumentNullException();
mode.Init();
_tabs.AddTab(mode);
mode.SelectedSampleChanged += ModeOnSelectedSampleChanged;
}
private void ModeOnSelectedSampleChanged(int frameIndex)
{
ViewFrameIndex = frameIndex;
}
/// <summary>
/// Clears data.
/// </summary>
public void Clear()
{
_frameIndex = -1;
_framesCount = 0;
for (int i = 0; i < _tabs.ChildrenCount; i++)
{
if (_tabs.Children[i] is ProfilerMode mode)
{
mode.Clear();
mode.UpdateView(ViewFrameIndex, _showOnlyLastUpdateEvents);
}
}
UpdateButtons();
}
private void OnSelectedTabChanged(Tabs tabs)
{
if (tabs.SelectedTab is ProfilerMode mode)
mode.UpdateView(ViewFrameIndex, _showOnlyLastUpdateEvents);
}
private void UpdateButtons()
{
_clearButton.Enabled = _framesCount > 0;
_prevFrameButton.Enabled = _frameIndex > 0;
_nextFrameButton.Enabled = (_framesCount - _frameIndex - 1) > 0;
_lastFrameButton.Enabled = _framesCount > 0;
_showOnlyLastUpdateEventsButton.Checked = _showOnlyLastUpdateEvents;
}
/// <inheritdoc />
public override void OnInit()
{
// Create default modes
AddMode(new Overall());
AddMode(new CPU());
AddMode(new GPU());
AddMode(new Memory());
// Init view
_frameIndex = -1;
for (int i = 0; i < _tabs.ChildrenCount; i++)
{
if (_tabs.Children[i] is ProfilerMode mode)
mode.UpdateView(ViewFrameIndex, _showOnlyLastUpdateEvents);
}
UpdateButtons();
}
/// <inheritdoc />
public override void OnUpdate()
{
if (LiveRecording)
{
FlaxEngine.Profiler.BeginEvent("ProfilerWindow.OnUpdate");
ProfilerMode.SharedUpdateData sharedData = new ProfilerMode.SharedUpdateData();
sharedData.Begin();
for (int i = 0; i < _tabs.ChildrenCount; i++)
{
if (_tabs.Children[i] is ProfilerMode mode)
mode.Update(ref sharedData);
}
sharedData.End();
_framesCount = Mathf.Min(_framesCount + 1, ProfilerMode.MaxSamples);
UpdateButtons();
FlaxEngine.Profiler.EndEvent();
}
}
/// <inheritdoc />
public override bool OnKeyDown(KeyboardKeys key)
{
if (base.OnKeyDown(key))
return true;
switch (key)
{
case KeyboardKeys.ArrowLeft:
ViewFrameIndex--;
return true;
case KeyboardKeys.ArrowRight:
ViewFrameIndex++;
return true;
}
return false;
}
}
}

View File

@@ -0,0 +1,103 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
namespace FlaxEditor.Windows.Profiler
{
/// <summary>
/// Profiler samples storage buffer. Support recording new frame samples.
/// </summary>
/// <typeparam name="T">Single sample data type.</typeparam>
public class SamplesBuffer<T>
{
private T[] _data;
private int _count;
/// <summary>
/// Gets the amount of samples in the buffer.
/// </summary>
public int Count => _count;
/// <summary>
/// Gets the last sample value. Check buffer <see cref="Count"/> before calling this property.
/// </summary>
public T Last => _data[_count - 1];
/// <summary>
/// Gets or sets the sample value at the specified index.
/// </summary>
/// <param name="index">The index.</param>
/// <returns>The sample value.</returns>
public T this[int index]
{
get => _data[index];
set => _data[index] = value;
}
/// <summary>
/// Initializes a new instance of the <see cref="SamplesBuffer{T}"/> class.
/// </summary>
/// <param name="capacity">The maximum buffer capacity.</param>
public SamplesBuffer(int capacity = ProfilerMode.MaxSamples)
{
_data = new T[capacity];
_count = 0;
}
/// <summary>
/// Gets the sample at the specified index or the last sample if index is equal to -1.
/// </summary>
/// <param name="index">The index.</param>
/// <returns>The sample value</returns>
public T Get(int index)
{
return index == -1 ? _data[_count - 1] : _data[index];
}
/// <summary>
/// Clears this buffer.
/// </summary>
public void Clear()
{
_count = 0;
}
/// <summary>
/// Adds the specified sample to the buffer.
/// </summary>
/// <param name="sample">The sample.</param>
public void Add(T sample)
{
// Remove first sample if no space
if (_count == _data.Length)
{
for (int i = 1; i < _count; i++)
{
_data[i - 1] = _data[i];
}
_count--;
}
_data[_count++] = sample;
}
/// <summary>
/// Adds the specified sample to the buffer.
/// </summary>
/// <param name="sample">The sample.</param>
public void Add(ref T sample)
{
// Remove first sample if no space
if (_count == _data.Length)
{
for (int i = 1; i < _count; i++)
{
_data[i - 1] = _data[i];
}
_count--;
}
_data[_count++] = sample;
}
}
}

View File

@@ -0,0 +1,195 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System;
using FlaxEngine;
using FlaxEngine.GUI;
namespace FlaxEditor.Windows.Profiler
{
/// <summary>
/// Draws simple chart.
/// </summary>
/// <seealso cref="FlaxEngine.GUI.Control" />
internal class SingleChart : Control
{
private const float TitleHeight = 20;
private const float PointsOffset = 4;
private readonly SamplesBuffer<float> _samples;
private string _sample;
private int _selectedSampleIndex = -1;
private bool _isSelecting;
/// <summary>
/// Gets or sets the chart title.
/// </summary>
public string Title { get; set; }
/// <summary>
/// Gets the index of the selected sample. Value -1 is used to indicate no selection (using the latest sample).
/// </summary>
public int SelectedSampleIndex
{
get => _selectedSampleIndex;
set
{
value = Mathf.Clamp(value, -1, _samples.Count - 1);
if (_selectedSampleIndex != value)
{
_selectedSampleIndex = value;
_sample = _samples.Count == 0 ? string.Empty : FormatSample(_samples.Get(_selectedSampleIndex));
SelectedSampleChanged?.Invoke(_selectedSampleIndex);
}
}
}
/// <summary>
/// Gets the selected sample value.
/// </summary>
public float SelectedSample => _samples.Get(_selectedSampleIndex);
/// <summary>
/// Occurs when selected sample gets changed.
/// </summary>
public event Action<int> SelectedSampleChanged;
/// <summary>
/// The handler function to format sample value for label text.
/// </summary>
public Func<float, string> FormatSample = (v) => v.ToString();
/// <summary>
/// Initializes a new instance of the <see cref="SingleChart"/> class.
/// </summary>
/// <param name="maxSamples">The maximum samples to collect.</param>
public SingleChart(int maxSamples = ProfilerMode.MaxSamples)
: base(0, 0, 100, 60 + TitleHeight)
{
_samples = new SamplesBuffer<float>(maxSamples);
_sample = string.Empty;
}
/// <summary>
/// Clears all the samples.
/// </summary>
public void Clear()
{
_samples.Clear();
_sample = string.Empty;
}
/// <summary>
/// Adds the sample value.
/// </summary>
/// <param name="value">The value.</param>
public void AddSample(float value)
{
_samples.Add(value);
_sample = FormatSample(value);
}
/// <inheritdoc />
public override void Draw()
{
base.Draw();
var style = Style.Current;
float chartHeight = Height - TitleHeight;
// Draw chart
if (_samples.Count > 0)
{
var chartRect = new Rectangle(0, 0, Width, chartHeight);
Render2D.PushClip(ref chartRect);
if (_selectedSampleIndex != -1)
{
float selectedX = Width - (_samples.Count - _selectedSampleIndex - 1) * PointsOffset;
Render2D.DrawLine(new Vector2(selectedX, 0), new Vector2(selectedX, chartHeight), Color.White, 1.5f);
}
int samplesInViewCount = Math.Min((int)(Width / PointsOffset), _samples.Count) - 1;
float maxValue = _samples[_samples.Count - 1];
for (int i = 2; i < samplesInViewCount; i++)
{
maxValue = Mathf.Max(maxValue, _samples[_samples.Count - i]);
}
Color chartColor = style.BackgroundSelected;
Vector2 chartRoot = chartRect.BottomRight;
float samplesRange = maxValue * 1.1f;
float samplesCoeff = -chartHeight / samplesRange;
Vector2 posPrev = chartRoot + new Vector2(0, _samples.Last * samplesCoeff);
float posX = 0;
for (int i = _samples.Count - 1; i >= 0; i--)
{
float sample = _samples[i];
Vector2 pos = chartRoot + new Vector2(posX, sample * samplesCoeff);
Render2D.DrawLine(posPrev, pos, chartColor);
posPrev = pos;
posX -= PointsOffset;
}
Render2D.PopClip();
}
// Draw title
var headerRect = new Rectangle(0, chartHeight, Width, TitleHeight);
var headerTextRect = new Rectangle(2, chartHeight, Width - 4, TitleHeight);
Render2D.FillRectangle(headerRect, style.BackgroundNormal);
Render2D.DrawText(style.FontMedium, Title, headerTextRect, Color.White * 0.8f, TextAlignment.Near, TextAlignment.Center);
Render2D.DrawText(style.FontMedium, _sample, headerTextRect, Color.White, TextAlignment.Far, TextAlignment.Center);
}
private void OnClick(ref Vector2 location)
{
float samplesWidth = _samples.Count * PointsOffset;
SelectedSampleIndex = (int)((samplesWidth - Width + location.X) / PointsOffset);
}
/// <inheritdoc />
public override bool OnMouseDown(Vector2 location, MouseButton button)
{
if (button == MouseButton.Left && location.Y < (Height - TitleHeight))
{
_isSelecting = true;
OnClick(ref location);
StartMouseCapture();
return true;
}
return base.OnMouseDown(location, button);
}
/// <inheritdoc />
public override void OnMouseMove(Vector2 location)
{
if (_isSelecting)
{
OnClick(ref location);
}
base.OnMouseMove(location);
}
/// <inheritdoc />
public override bool OnMouseUp(Vector2 location, MouseButton button)
{
if (button == MouseButton.Left && _isSelecting)
{
_isSelecting = false;
EndMouseCapture();
return true;
}
return base.OnMouseUp(location, button);
}
/// <inheritdoc />
public override void OnEndMouseCapture()
{
_isSelecting = false;
}
}
}

View File

@@ -0,0 +1,139 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using FlaxEngine;
using FlaxEngine.GUI;
namespace FlaxEditor.Windows.Profiler
{
/// <summary>
/// Events timeline control.
/// </summary>
/// <seealso cref="FlaxEngine.GUI.Panel" />
public class Timeline : Panel
{
/// <summary>
/// Single timeline event control.
/// </summary>
/// <seealso cref="ContainerControl" />
public class Event : ContainerControl
{
private static readonly Color[] Colors =
{
new Color(0.8f, 0.894117653f, 0.709803939f, 1f),
new Color(0.1254902f, 0.698039234f, 0.6666667f, 1f),
new Color(0.4831376f, 0.6211768f, 0.0219608f, 1f),
new Color(0.3827448f, 0.2886272f, 0.5239216f, 1f),
new Color(0.8f, 0.4423528f, 0f, 1f),
new Color(0.4486272f, 0.4078432f, 0.050196f, 1f),
new Color(0.4831376f, 0.6211768f, 0.0219608f, 1f),
new Color(0.4831376f, 0.6211768f, 0.0219608f, 1f),
new Color(0.2070592f, 0.5333336f, 0.6556864f, 1f),
new Color(0.8f, 0.4423528f, 0f, 1f),
new Color(0.4486272f, 0.4078432f, 0.050196f, 1f),
new Color(0.7749016f, 0.6368624f, 0.0250984f, 1f),
new Color(0.5333336f, 0.16f, 0.0282352f, 1f),
new Color(0.3827448f, 0.2886272f, 0.5239216f, 1f),
new Color(0.478431374f, 0.482352942f, 0.117647059f, 1f),
new Color(0.9411765f, 0.5019608f, 0.5019608f, 1f),
new Color(0.6627451f, 0.6627451f, 0.6627451f, 1f),
new Color(0.545098066f, 0f, 0.545098066f, 1f),
};
private Color _color;
private float _nameLength;
/// <summary>
/// The default height of the event.
/// </summary>
public const float DefaultHeight = 25.0f;
/// <summary>
/// Gets or sets the event name.
/// </summary>
public string Name { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="Event"/> class.
/// </summary>
/// <param name="x">The x position.</param>
/// <param name="depth">The timeline row index (event depth).</param>
/// <param name="width">The width.</param>
public Event(float x, int depth, float width)
: base(x, depth * DefaultHeight, width, DefaultHeight - 1)
{
_nameLength = -1;
}
/// <inheritdoc />
protected override void OnParentChangedInternal()
{
base.OnParentChangedInternal();
int key = (HasParent ? Parent.GetChildIndex(this) : 1) * (string.IsNullOrEmpty(Name) ? 1 : Name[0]);
_color = Colors[key % Colors.Length] * 0.8f;
}
/// <inheritdoc />
public override void Draw()
{
base.Draw();
var style = Style.Current;
var bounds = new Rectangle(Vector2.Zero, Size);
Color color = _color;
if (IsMouseOver)
color *= 1.1f;
Render2D.FillRectangle(bounds, color);
Render2D.DrawRectangle(bounds, color * 0.5f);
if (_nameLength < 0 && style.FontMedium)
_nameLength = style.FontMedium.MeasureText(Name).X;
if (_nameLength < bounds.Width + 4)
{
Render2D.PushClip(bounds);
Render2D.DrawText(style.FontMedium, Name, bounds, Color.White, TextAlignment.Center, TextAlignment.Center);
Render2D.PopClip();
}
}
}
/// <summary>
/// Timeline track label
/// </summary>
/// <seealso cref="FlaxEngine.GUI.ContainerControl" />
public class TrackLabel : ContainerControl
{
/// <summary>
/// Gets or sets the name.
/// </summary>
public string Name { get; set; }
/// <inheritdoc />
public override void Draw()
{
base.Draw();
var style = Style.Current;
var rect = new Rectangle(Vector2.Zero, Size);
Render2D.PushClip(rect);
Render2D.DrawText(style.FontMedium, Name, rect, Color.White, TextAlignment.Center, TextAlignment.Center, TextWrapping.WrapChars);
Render2D.PopClip();
}
}
/// <summary>
/// Gets the events container control. Use it to remove/add events to the timeline.
/// </summary>
public ContainerControl EventsContainer => this;
/// <summary>
/// Initializes a new instance of the <see cref="Timeline"/> class.
/// </summary>
public Timeline()
: base(ScrollBars.Both)
{
}
}
}