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;
5using System.Collections.Generic;
6using System.Linq;
7using NUnit.Framework;
8using osu.Framework.Graphics;
9using osu.Framework.Graphics.Containers;
10using osu.Framework.Graphics.Pooling;
11using osu.Framework.Graphics.Shapes;
12using osu.Framework.Graphics.Sprites;
13using osu.Framework.Testing;
14using osu.Framework.Timing;
15using osu.Framework.Utils;
16using osuTK;
17using osuTK.Graphics;
18
19namespace osu.Framework.Tests.Visual.Drawables
20{
21 public class TestSceneDrawablePool : TestScene
22 {
23 private DrawablePool<TestDrawable> pool;
24 private SpriteText count;
25
26 private readonly HashSet<TestDrawable> consumed = new HashSet<TestDrawable>();
27
28 [Test]
29 public void TestPoolInitialDrawableLoadedAheadOfTime()
30 {
31 const int pool_size = 3;
32 resetWithNewPool(() => new TestPool(TimePerAction, pool_size));
33
34 for (int i = 0; i < 3; i++)
35 AddAssert("check drawable is in ready state", () => pool.Get().LoadState == LoadState.Ready);
36 }
37
38 [Test]
39 public void TestPoolUsageWithinLimits()
40 {
41 const int pool_size = 10;
42
43 resetWithNewPool(() => new TestPool(TimePerAction, pool_size));
44
45 AddRepeatStep("get new pooled drawable", () => consumeDrawable(), 50);
46
47 AddUntilStep("all returned to pool", () => pool.CountAvailable == pool_size);
48
49 AddAssert("consumed drawables report returned to pool", () => consumed.All(d => d.IsInPool));
50 AddAssert("consumed drawables not disposed", () => consumed.All(d => !d.IsDisposed));
51
52 AddAssert("consumed less than pool size", () => consumed.Count < pool_size);
53 }
54
55 [Test]
56 public void TestPoolUsageExceedsLimits()
57 {
58 const int pool_size = 10;
59
60 resetWithNewPool(() => new TestPool(TimePerAction * 20, pool_size));
61
62 AddRepeatStep("get new pooled drawable", () => consumeDrawable(), 50);
63
64 AddUntilStep("all returned to pool", () => pool.CountAvailable == consumed.Count);
65
66 AddAssert("pool grew in size", () => pool.CountAvailable > pool_size);
67
68 AddAssert("consumed drawables report returned to pool", () => consumed.All(d => d.IsInPool));
69 AddAssert("consumed drawables not disposed", () => consumed.All(d => !d.IsDisposed));
70 }
71
72 [TestCase(10)]
73 [TestCase(20)]
74 public void TestPoolInitialSize(int initialPoolSize)
75 {
76 resetWithNewPool(() => new TestPool(TimePerAction * 20, initialPoolSize));
77
78 AddUntilStep("available count is correct", () => pool.CountAvailable == initialPoolSize);
79 }
80
81 [Test]
82 public void TestReturnWithoutAdding()
83 {
84 resetWithNewPool(() => new TestPool(TimePerAction, 1));
85
86 TestDrawable drawable = null;
87
88 AddStep("consume without adding", () => drawable = pool.Get());
89
90 AddStep("manually return", () => drawable.Return());
91
92 AddUntilStep("free was run", () => drawable.FreedCount == 1);
93 AddUntilStep("was returned", () => pool.CountAvailable == 1);
94
95 AddAssert("manually return twice throws", () =>
96 {
97 try
98 {
99 drawable.Return();
100 return false;
101 }
102 catch (InvalidOperationException)
103 {
104 return true;
105 }
106 });
107 }
108
109 [Test]
110 public void TestPoolReturnWhenAboveCapacity()
111 {
112 resetWithNewPool(() => new TestPool(TimePerAction * 20, 1, 1));
113
114 TestDrawable first = null, second = null;
115
116 AddStep("consume item", () => first = consumeDrawable());
117
118 AddAssert("pool is empty", () => pool.CountAvailable == 0);
119
120 AddStep("consume and return another item", () =>
121 {
122 second = pool.Get();
123 second.Return();
124 });
125
126 AddAssert("first item still in use", () => first.IsInUse);
127
128 AddUntilStep("second is returned", () => !second.IsInUse && pool.CountAvailable == 1);
129
130 AddStep("expire first", () => first.Expire());
131
132 AddUntilStep("wait until first dead", () => !first.IsAlive);
133 AddUntilStep("drawable is disposed", () => first.IsDisposed);
134 }
135
136 [Test]
137 public void TestPrepareAndFreeMethods()
138 {
139 resetWithNewPool(() => new TestPool(TimePerAction, 1));
140
141 TestDrawable drawable = null;
142 TestDrawable drawable2 = null;
143
144 AddStep("consume item", () => drawable = consumeDrawable());
145
146 AddAssert("prepare was run", () => drawable.PreparedCount == 1);
147 AddUntilStep("free was run", () => drawable.FreedCount == 1);
148
149 AddStep("consume item", () => drawable2 = consumeDrawable());
150
151 AddAssert("is same item", () => ReferenceEquals(drawable, drawable2));
152
153 AddAssert("prepare was run", () => drawable2.PreparedCount == 2);
154 AddUntilStep("free was run", () => drawable2.FreedCount == 2);
155 }
156
157 [Test]
158 public void TestPrepareOnlyOnceOnMultipleUsages()
159 {
160 resetWithNewPool(() => new TestPool(TimePerAction, 1));
161
162 TestDrawable drawable = null;
163 TestDrawable drawable2 = null;
164
165 AddStep("consume item", () => drawable = consumeDrawable(false));
166
167 AddAssert("prepare was not run", () => drawable.PreparedCount == 0);
168 AddUntilStep("free was not run", () => drawable.FreedCount == 0);
169
170 AddStep("manually return drawable", () => pool.Return(drawable));
171 AddUntilStep("free was run", () => drawable.FreedCount == 1);
172
173 AddStep("consume item", () => drawable2 = consumeDrawable());
174
175 AddAssert("is same item", () => ReferenceEquals(drawable, drawable2));
176
177 AddAssert("prepare was only run once", () => drawable2.PreparedCount == 1);
178 AddUntilStep("free was run", () => drawable2.FreedCount == 2);
179 }
180
181 [Test]
182 public void TestUsePoolableDrawableWithoutPool()
183 {
184 TestDrawable drawable = null;
185
186 AddStep("consume item", () => Add(drawable = new TestDrawable()));
187
188 AddAssert("prepare was run", () => drawable.PreparedCount == 1);
189 AddUntilStep("free was run", () => drawable.FreedCount == 1);
190
191 AddUntilStep("drawable was disposed", () => drawable.IsDisposed);
192 }
193
194 [Test]
195 public void TestAllDrawablesComeReady()
196 {
197 const int pool_size = 10;
198 List<Drawable> retrieved = new List<Drawable>();
199
200 resetWithNewPool(() => new TestPool(TimePerAction * 20, 10, pool_size));
201
202 AddStep("get many pooled drawables", () =>
203 {
204 retrieved.Clear();
205 for (int i = 0; i < pool_size * 2; i++)
206 retrieved.Add(pool.Get());
207 });
208
209 AddAssert("all drawables in ready state", () => retrieved.All(d => d.LoadState == LoadState.Ready));
210 }
211
212 [TestCase(10)]
213 [TestCase(20)]
214 public void TestPoolUsageExceedsMaximum(int maxPoolSize)
215 {
216 resetWithNewPool(() => new TestPool(TimePerAction * 20, 10, maxPoolSize));
217
218 AddStep("get many pooled drawables", () =>
219 {
220 for (int i = 0; i < maxPoolSize * 2; i++)
221 consumeDrawable();
222 });
223
224 AddAssert("pool saturated", () => pool.CountAvailable == 0);
225
226 AddUntilStep("pool size returned to correct maximum", () => pool.CountAvailable == maxPoolSize);
227 AddUntilStep("count in pool is correct", () => consumed.Count(d => d.IsInPool) == maxPoolSize);
228 AddAssert("excess drawables were used", () => consumed.Any(d => !d.IsInPool));
229 AddUntilStep("non-returned drawables disposed", () => consumed.Where(d => !d.IsInPool).All(d => d.IsDisposed));
230 }
231
232 [Test]
233 public void TestGetFromNotLoadedPool()
234 {
235 Assert.DoesNotThrow(() => new TestPool(100, 1).Get());
236 }
237
238 /// <summary>
239 /// Tests that when a child of a pooled drawable receives a parent invalidation, the parent pooled drawable is not returned.
240 /// A parent invalidation can happen on the child if it's added to the hierarchy of the parent.
241 /// </summary>
242 [Test]
243 public void TestParentInvalidationFromChildDoesNotReturnPooledParent()
244 {
245 resetWithNewPool(() => new TestPool(TimePerAction, 1));
246
247 TestDrawable drawable = null;
248
249 AddStep("consume item", () => drawable = consumeDrawable(false));
250 AddStep("add child", () => drawable.AddChild(Empty()));
251 AddAssert("not freed", () => drawable.FreedCount == 0);
252 }
253
254 [Test]
255 public void TestDrawablePreparedWhenClockRewound()
256 {
257 resetWithNewPool(() => new TestPool(TimePerAction, 1));
258
259 TestDrawable drawable = null;
260
261 AddStep("consume item and rewind clock", () =>
262 {
263 var clock = new ManualClock { CurrentTime = Time.Current };
264
265 Add(new Container
266 {
267 RelativeSizeAxes = Axes.Both,
268 Clock = new FramedClock(clock),
269 Child = drawable = consumeDrawable(false)
270 });
271
272 clock.CurrentTime = 0;
273 });
274
275 AddAssert("child prepared", () => drawable.PreparedCount == 1);
276 }
277
278 protected override void Update()
279 {
280 base.Update();
281 if (count != null)
282 count.Text = $"available: {pool.CountAvailable} consumed: {consumed.Count} disposed: {consumed.Count(d => d.IsDisposed)}";
283 }
284
285 private static int displayCount;
286
287 private TestDrawable consumeDrawable(bool addToHierarchy = true)
288 {
289 var drawable = pool.Get(d =>
290 {
291 d.Position = new Vector2(RNG.NextSingle(), RNG.NextSingle());
292 d.DisplayString = (++displayCount).ToString();
293 });
294
295 consumed.Add(drawable);
296 if (addToHierarchy)
297 Add(drawable);
298
299 return drawable;
300 }
301
302 private void resetWithNewPool(Func<DrawablePool<TestDrawable>> createPool)
303 {
304 AddStep("reset stats", () => consumed.Clear());
305
306 AddStep("create pool", () =>
307 {
308 pool = createPool();
309
310 Children = new Drawable[]
311 {
312 pool,
313 count = new SpriteText(),
314 };
315 });
316 }
317
318 private class TestPool : DrawablePool<TestDrawable>
319 {
320 private readonly double fadeTime;
321
322 public TestPool(double fadeTime, int initialSize, int? maximumSize = null)
323 : base(initialSize, maximumSize)
324 {
325 this.fadeTime = fadeTime;
326 }
327
328 protected override TestDrawable CreateNewDrawable()
329 {
330 return new TestDrawable(fadeTime);
331 }
332 }
333
334 private class TestDrawable : PoolableDrawable
335 {
336 private readonly double fadeTime;
337
338 private readonly SpriteText text;
339
340 public string DisplayString
341 {
342 set => text.Text = value;
343 }
344
345 public TestDrawable()
346 : this(1000)
347 {
348 }
349
350 public TestDrawable(double fadeTime)
351 {
352 this.fadeTime = fadeTime;
353
354 RelativePositionAxes = Axes.Both;
355 Size = new Vector2(50);
356 Origin = Anchor.Centre;
357
358 InternalChildren = new Drawable[]
359 {
360 new Box
361 {
362 Colour = Color4.Green,
363 RelativeSizeAxes = Axes.Both,
364 },
365 text = new SpriteText
366 {
367 Text = "-",
368 Font = FontUsage.Default.With(size: 40),
369 Anchor = Anchor.Centre,
370 Origin = Anchor.Centre,
371 },
372 };
373 }
374
375 public void AddChild(Drawable drawable) => AddInternal(drawable);
376
377 public new bool IsDisposed => base.IsDisposed;
378
379 public int PreparedCount { get; private set; }
380 public int FreedCount { get; private set; }
381
382 protected override void PrepareForUse()
383 {
384 this.FadeOutFromOne(fadeTime);
385 this.RotateTo(0).RotateTo(80, fadeTime);
386
387 Expire();
388
389 PreparedCount++;
390 }
391
392 protected override void FreeAfterUse()
393 {
394 base.FreeAfterUse();
395 FreedCount++;
396 }
397 }
398 }
399}