using System.Collections.Generic; using System.Runtime.InteropServices; using Robust.Client.AutoGenerated; using Robust.Client.Profiling; using Robust.Client.UserInterface.XAML; using Robust.Shared.Collections; using Robust.Shared.IoC; using Robust.Shared.Log; using Robust.Shared.Maths; using Robust.Shared.Profiling; namespace Robust.Client.UserInterface; [GenerateTypedNameReferences] internal sealed partial class ProfTree : Control { public const float TreeLevelMargin = 16; [Dependency] private ProfManager _prof = default!; [Dependency] private ProfViewManager _profViewMgr = default!; private ProfViewManager.Snapshot? _snapshot; public long Frame; private readonly TreeExpand _treeExpand = new(); public ProfTree() { RobustXamlLoader.Load(this); IoCManager.InjectDependencies(this); Header.Text.Text = "Thing"; Header.SideLabel.Text = "Misc"; Header.TimeLabel.Text = "Time"; Header.PercentTimeLabel.Text = "Time%"; Header.AllocLabel.Text = "Alloc"; Header.PercentAllocLabel.Text = "Alloc%"; } public void LoadSnapshot(ProfViewManager.Snapshot snapshot) { _snapshot = snapshot; } public void RebuildTree() { if (_snapshot == null) return; TreeRoot.RemoveAllChildren(); ref var buf = ref _snapshot.Buffer; var indexIdx = _profViewMgr.GetIndexOfFrame(Frame, _snapshot); if (indexIdx == 0) { Logger.WarningS("prof.ui", $"Unable to find index for frame: {Frame}"); return; } // Index for this frame's data. ref var index = ref _snapshot.Buffer.Index(indexIdx); // The frame must be wrapped in a whole group. We display this group. var i = index.EndPos - 1; ref var logEnd = ref buf.Log(i); var controls = new ValueList(); // Create child controls. var countDict = new Dictionary(); var totalFrameTime = ProfGraphView.GetFrameTime(buf, index); var (totalCounts, _) = GetTotalGroupCounts(buf, i, logEnd.GroupEnd.StartIndex); BuildData data = default; data.Buffer = buf; data.Index = index; data.TotalFrameTime = totalFrameTime; // We traverse the log backwards because we can use GroupEnd nodes to skip over data entirely. for (; i >= index.StartPos; i--) { // Recursively build the tree. ref var log = ref buf.Log(i); RebuildTreeAddControls( in data, ref controls, ref i, in log, totalCounts, countDict, _treeExpand, 2); } // Because we traversed backwards, the control list is backwards too. // Insert backwards again to fix that. for (var c = controls.Count - 1; c >= 0; c--) { TreeRoot.AddChild(controls[c]); } // Do a final pass to assign the alternating background colors. // Easier to do here than to do it while we're creating the controls. var alt = false; foreach (var child in TreeRoot.Children) { DoAltBackgrounds(child, ref alt); } } private static void DoAltBackgrounds(Control control, ref bool alt) { if (control is not ProfAltBackground altBg) return; altBg.IsAltBackground = alt; alt = !alt; } private void RebuildTreeAddControls( in BuildData data, ref ValueList insertInto, ref long i, in ProfLog log, Dictionary totalCounts, Dictionary countDict, TreeExpand expandParent, float margin) { switch (log.Type) { case ProfLogType.Value: TreeInsertSample(in data, ref insertInto, log.Value.StringId, log.Value.Value, margin); break; case ProfLogType.GroupEnd: var stringId = log.GroupEnd.StringId; ref var count = ref CollectionsMarshal.GetValueRefOrAddDefault(countDict, stringId, out _); count += 1; var totalCount = totalCounts[stringId]; var stringI = totalCount - count; TreeInsertGroup( in data, ref insertInto, ref i, in log.GroupEnd, (stringId, stringI), expandParent, margin); break; } } private void TreeInsertSample( in BuildData data, ref ValueList insertInto, int stringId, in ProfValue value, float margin) { var treeLine = new ProfTreeLine(); treeLine.Margin = new Thickness(treeLine.Margin.Left + margin + 12 + 2, 0, 0, 0); FillTreeLine(in data, treeLine, stringId, value); insertInto.Add(new ProfAltBackground { Children = { treeLine } }); } private void TreeInsertGroup( in BuildData data, ref ValueList insertInto, ref long i, in ProfLogGroupEnd logEnd, (int str, int i) expandId, TreeExpand expandParent, float margin) { i -= 1; var (totalCounts, anyChildren) = GetTotalGroupCounts(data.Buffer, i, logEnd.StartIndex); if (!anyChildren) { // Node has no children we can display. Just insert it as if it's a sample and move along. TreeInsertSample(in data, ref insertInto, logEnd.StringId, logEnd.Value, margin); i = logEnd.StartIndex; return; } var groupControl = new ProfTreeEntry(this, expandParent, expandId, margin); var altBg = new ProfAltBackground { Children = { groupControl } }; FillTreeLine(in data, groupControl.TextLine, logEnd.StringId, logEnd.Value); // Look up in the parent's expand data whether we're expanded. var expand = expandParent.ExpandedItems.GetValueOrDefault(expandId); if (expand is not { Enabled: true }) { // Note expanded. Skip! i = logEnd.StartIndex; insertInto.Add(altBg); return; } groupControl.Arrow.Rotated = true; // Create child controls. var countDict = new Dictionary(); for (; i >= data.Index.StartPos; i--) { ref var log = ref data.Buffer.Log(i); if (log.Type == ProfLogType.GroupStart) break; RebuildTreeAddControls( in data, ref insertInto, ref i, in log, totalCounts, countDict, expand, margin + TreeLevelMargin); } insertInto.Add(altBg); } private static (Dictionary counts, bool anyChildren) GetTotalGroupCounts( in ProfBuffer buffer, long i, long startIdx) { var anyChildren = false; var dict = new Dictionary(); for (; i > startIdx; i--) { ref var log = ref buffer.Log(i); anyChildren |= log.Type is ProfLogType.GroupEnd or ProfLogType.Value; if (log.Type != ProfLogType.GroupEnd) continue; i = log.GroupEnd.StartIndex; ref var val = ref CollectionsMarshal.GetValueRefOrAddDefault(dict, log.GroupEnd.StringId, out _); val += 1; } return (dict, anyChildren); } private void FillTreeLine( in BuildData data, ProfTreeLine line, int stringId, in ProfValue value) { line.Text.Text = _prof.GetString(stringId); switch (value.Type) { case ProfValueType.TimeAllocSample: line.TimeLabel.Text = $"{value.TimeAllocSample.Time * 1000:N2} ms"; line.PercentTimeLabel.Text = $"{value.TimeAllocSample.Time / data.TotalFrameTime.Time:P2}"; line.AllocLabel.Text = $"{value.TimeAllocSample.Alloc} B"; line.PercentAllocLabel.Text = $"{value.TimeAllocSample.Alloc / (float)data.TotalFrameTime.Alloc:P2}"; break; case ProfValueType.Int32: line.SideLabel.Text = value.Int32.ToString(); break; case ProfValueType.Int64: line.SideLabel.Text = value.Int64.ToString(); break; default: line.SideLabel.Text = "???"; break; } } internal sealed class TreeExpand { public bool Enabled = true; public readonly Dictionary<(int str, int i), TreeExpand> ExpandedItems = new(); } private struct BuildData { public ProfBuffer Buffer; public ProfIndex Index; public TimeAndAllocSample TotalFrameTime; } }