A game framework written with osu! in mind.
1// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
2// See the LICENCE file in the repository root for full licence text.
3
4using System.Collections.Generic;
5using System.Collections.Specialized;
6using System.Linq;
7using osu.Framework.Allocation;
8using osu.Framework.Bindables;
9using osu.Framework.Graphics.Containers;
10using osu.Framework.Graphics.Sprites;
11using osu.Framework.Graphics.Visualisation;
12using osu.Framework.Platform;
13using osu.Framework.Statistics;
14
15namespace osu.Framework.Graphics.Performance
16{
17 /// <summary>
18 /// Tracks global game statistics.
19 /// </summary>
20 internal class GlobalStatisticsDisplay : ToolWindow
21 {
22 private readonly FillFlowContainer<StatisticsGroup> groups;
23
24 private DotNetRuntimeListener listener;
25
26 private Bindable<bool> performanceLogging;
27
28 public GlobalStatisticsDisplay()
29 : base("Global Statistics", "(Ctrl+F2 to toggle)")
30 {
31 ScrollContent.Children = new Drawable[]
32 {
33 groups = new AlphabeticalFlow<StatisticsGroup>
34 {
35 Padding = new MarginPadding(5),
36 RelativeSizeAxes = Axes.X,
37 AutoSizeAxes = Axes.Y,
38 Direction = FillDirection.Vertical,
39 },
40 };
41 }
42
43 [BackgroundDependencyLoader]
44 private void load(GameHost host)
45 {
46 performanceLogging = host.PerformanceLogging.GetBoundCopy();
47 }
48
49 protected override void LoadComplete()
50 {
51 base.LoadComplete();
52
53 GlobalStatistics.StatisticsChanged += (_, e) =>
54 {
55 switch (e.Action)
56 {
57 case NotifyCollectionChangedAction.Add:
58 add(e.NewItems.Cast<IGlobalStatistic>());
59 break;
60
61 case NotifyCollectionChangedAction.Remove:
62 remove(e.OldItems.Cast<IGlobalStatistic>());
63 break;
64 }
65 };
66
67 add(GlobalStatistics.GetStatistics());
68
69 State.BindValueChanged(visibilityChanged, true);
70 }
71
72 private void visibilityChanged(ValueChangedEvent<Visibility> state)
73 {
74 performanceLogging.Value = state.NewValue == Visibility.Visible;
75
76 if (state.NewValue == Visibility.Visible)
77 {
78 GlobalStatistics.OutputToLog();
79 listener = new DotNetRuntimeListener();
80 }
81 else
82 listener?.Dispose();
83 }
84
85 private void remove(IEnumerable<IGlobalStatistic> stats) => Schedule(() =>
86 {
87 foreach (var stat in stats)
88 groups.FirstOrDefault(g => g.GroupName == stat.Group)?.Remove(stat);
89 });
90
91 private void add(IEnumerable<IGlobalStatistic> stats) => Schedule(() =>
92 {
93 foreach (var stat in stats)
94 {
95 var group = groups.FirstOrDefault(g => g.GroupName == stat.Group);
96
97 if (group == null)
98 groups.Add(group = new StatisticsGroup(stat.Group));
99 group.Add(stat);
100 }
101 });
102
103 private class StatisticsGroup : CompositeDrawable, IAlphabeticalSort
104 {
105 public string GroupName { get; }
106
107 private readonly FillFlowContainer<StatisticsItem> items;
108
109 public StatisticsGroup(string groupName)
110 {
111 GroupName = groupName;
112
113 RelativeSizeAxes = Axes.X;
114 AutoSizeAxes = Axes.Y;
115
116 InternalChildren = new Drawable[]
117 {
118 new FillFlowContainer
119 {
120 RelativeSizeAxes = Axes.X,
121 AutoSizeAxes = Axes.Y,
122 Direction = FillDirection.Vertical,
123 Children = new Drawable[]
124 {
125 new SpriteText
126 {
127 Text = GroupName,
128 Font = FrameworkFont.Regular.With(weight: "Bold")
129 },
130 items = new AlphabeticalFlow<StatisticsItem>
131 {
132 Padding = new MarginPadding { Left = 5 },
133 RelativeSizeAxes = Axes.X,
134 AutoSizeAxes = Axes.Y,
135 Direction = FillDirection.Vertical,
136 },
137 }
138 },
139 };
140 }
141
142 public void Add(IGlobalStatistic stat)
143 {
144 if (items.Any(s => s.Statistic == stat))
145 return;
146
147 items.Add(new StatisticsItem(stat));
148 }
149
150 public void Remove(IGlobalStatistic stat)
151 {
152 items.FirstOrDefault(s => s.Statistic == stat)?.Expire();
153 }
154
155 private class StatisticsItem : CompositeDrawable, IAlphabeticalSort
156 {
157 public readonly IGlobalStatistic Statistic;
158
159 private readonly SpriteText valueText;
160
161 public StatisticsItem(IGlobalStatistic statistic)
162 {
163 Statistic = statistic;
164
165 RelativeSizeAxes = Axes.X;
166 AutoSizeAxes = Axes.Y;
167
168 InternalChildren = new Drawable[]
169 {
170 new SpriteText
171 {
172 Font = FrameworkFont.Regular,
173 Colour = FrameworkColour.Yellow,
174 Text = Statistic.Name,
175 RelativeSizeAxes = Axes.X,
176 Width = 0.68f,
177 },
178 valueText = new SpriteText
179 {
180 Font = FrameworkFont.Condensed,
181 RelativePositionAxes = Axes.X,
182 X = 0.7f,
183 RelativeSizeAxes = Axes.X,
184 Width = 0.3f,
185 },
186 };
187 }
188
189 public string SortString => Statistic.Name;
190
191 protected override void Update()
192 {
193 base.Update();
194 valueText.Text = Statistic.DisplayValue;
195 }
196 }
197
198 public string SortString => GroupName;
199 }
200
201 private interface IAlphabeticalSort
202 {
203 string SortString { get; }
204 }
205
206 private class AlphabeticalFlow<T> : FillFlowContainer<T> where T : Drawable, IAlphabeticalSort
207 {
208 public override IEnumerable<Drawable> FlowingChildren => base.FlowingChildren.Cast<T>().OrderBy(d => d.SortString);
209 }
210
211 protected override void Dispose(bool isDisposing)
212 {
213 base.Dispose(isDisposing);
214 listener?.Dispose();
215 }
216 }
217}