A game framework written with osu! in mind.
at master 399 lines 13 kB view raw
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}