// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. using System; using System.Collections.Generic; using FlaxEditor.GUI; using FlaxEngine; using FlaxEngine.GUI; namespace FlaxEngine { partial class ProfilerCPU { partial struct Event { /// /// Gets the event name. /// public unsafe string Name { get { fixed (short* name = Name0) { return new string((char*)name); } } } internal unsafe bool NameStartsWith(string prefix) { fixed (short* name = Name0) { fixed (char* p = prefix) { return Utils.MemoryCompare(new IntPtr(name), new IntPtr(p), (ulong)(prefix.Length * 2)) == 0; } } } } } } namespace FlaxEditor.Windows.Profiler { /// /// The CPU performance profiling mode. /// /// internal sealed unsafe class CPU : ProfilerMode { private readonly SingleChart _mainChart; private readonly Timeline _timeline; private readonly Table _table; private SamplesBuffer _events; private List _timelineLabelsCache; private List _timelineEventsCache; private List _tableRowsCache; 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); } /// public override void Clear() { _mainChart.Clear(); _events?.Clear(); } /// public override void Update(ref SharedUpdateData sharedData) { _mainChart.AddSample(sharedData.Stats.UpdateTimeMs); // Gather CPU events var events = sharedData.GetEventsCPU(); if (_events == null) _events = new SamplesBuffer(); _events.Add(events); } /// public override void UpdateView(int selectedFrame, bool showOnlyLastUpdateEvents) { _showOnlyLastUpdateEvents = showOnlyLastUpdateEvents; _mainChart.SelectedSampleIndex = selectedFrame; if (_events == null) return; if (_timelineLabelsCache == null) _timelineLabelsCache = new List(); if (_timelineEventsCache == null) _timelineEventsCache = new List(); if (_tableRowsCache == null) _tableRowsCache = new List(); var viewRange = _showOnlyLastUpdateEvents ? GetMainThreadUpdateRange() : ViewRange.Full; UpdateTimeline(ref viewRange); UpdateTable(ref viewRange); } /// public override void OnDestroy() { _timelineLabelsCache?.Clear(); _timelineEventsCache?.Clear(); _tableRowsCache?.Clear(); base.OnDestroy(); } 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 GetMainThreadUpdateRange() { if (_events != null && _events.Count != 0) { var threads = _events.Get(_mainChart.SelectedSampleIndex); if (threads != null) { for (int j = 0; j < threads.Length; j++) { var thread = threads[j]; if (thread.Name != "Main" || thread.Events == null) continue; for (int i = 0; i < thread.Events.Length; i++) { ref var e = ref thread.Events[i]; if (e.Depth == 0 && 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; if (length <= 0.0) return; double scale = 100.0; float x = (float)((e.Start - startTime) * scale); float width = (float)(length * scale); Timeline.Event control; if (_timelineEventsCache.Count != 0) { var last = _timelineEventsCache.Count - 1; control = _timelineEventsCache[last]; _timelineEventsCache.RemoveAt(last); } else { control = new Timeline.Event(); } control.Bounds = new Rectangle(x + xOffset, (e.Depth + depthOffset) * Timeline.Event.DefaultHeight, width, Timeline.Event.DefaultHeight - 1); control.Name = e.Name.Replace("::", "."); control.TooltipText = string.Format("{0}, {1} ms", control.Name, ((int)(length * 1000.0) / 1000.0f)); control.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; container.IsLayoutLocked = true; int idx = 0; while (container.Children.Count > idx) { var child = container.Children[idx]; if (child is Timeline.Event e) { _timelineEventsCache.Add(e); child.Parent = null; } else if (child is Timeline.TrackLabel l) { _timelineLabelsCache.Add(l); child.Parent = null; } else { idx++; } } _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; if (viewRange.Start > 0) { startTime = viewRange.Start; } else { var r = GetMainThreadUpdateRange(); if (r.Start > 0) { startTime = r.Start; } else { 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]; 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; Timeline.TrackLabel trackLabel; if (_timelineLabelsCache.Count != 0) { var last = _timelineLabelsCache.Count - 1; trackLabel = _timelineLabelsCache[last]; _timelineLabelsCache.RemoveAt(last); } else { trackLabel = new Timeline.TrackLabel(); } trackLabel.Bounds = new Rectangle(0, depthOffset * Timeline.Event.DefaultHeight, xOffset, (maxDepth + 2) * Timeline.Event.DefaultHeight); trackLabel.Name = data[i].Name; trackLabel.BackgroundColor = Style.Current.Background * 1.1f; trackLabel.Parent = container; // Add events for (int j = 0; j < events.Length; j++) { var e = events[j]; if (e.Depth == 0) { 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.IsLayoutLocked = true; RecycleTableRows(_table, _tableRowsCache); 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); if (e.End <= 0.0f || 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 && e.End > 0.0f) { subEventsTimeTotal += Math.Max(sub.End - sub.Start, MinEventTimeMs); } else if (sub.Depth <= e.Depth) { break; } subEventsMemoryTotal += sub.ManagedMemoryAllocation + e.NativeMemoryAllocation; } string name = e.Name.Replace("::", "."); Row row; if (_tableRowsCache.Count != 0) { var last = _tableRowsCache.Count - 1; row = _tableRowsCache[last]; _tableRowsCache.RemoveAt(last); } else { row = new Row { Values = new object[6], }; } { // Event row.Values[0] = name; // Total (%) row.Values[1] = (int)(time / totalTimeMs * 1000.0f) / 10.0f; // Self (%) row.Values[2] = (int)((time - subEventsTimeTotal) / time * 1000.0f) / 10.0f; // Time ms row.Values[3] = (float)((time * 10000.0f) / 10000.0f); // Self ms row.Values[4] = (float)(((time - subEventsTimeTotal) * 10000.0f) / 10000.0f); // Memory Alloc row.Values[5] = subEventsMemoryTotal; } row.Depth = e.Depth; row.Width = _table.Width; row.Visible = e.Depth < 2; row.BackgroundColor = i % 2 == 0 ? rowColor2 : Color.Transparent; row.Parent = _table; } } } } }