A game framework written with osu! in mind.
at master 535 lines 19 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 System.Threading; 8using NUnit.Framework; 9using osu.Framework.Allocation; 10using osu.Framework.Graphics; 11using osu.Framework.Graphics.Containers; 12using osu.Framework.Graphics.Sprites; 13using osu.Framework.Lists; 14using osu.Framework.Threading; 15using osuTK; 16using osuTK.Graphics; 17 18namespace osu.Framework.Tests.Visual.Drawables 19{ 20 public class TestSceneDelayedLoadUnloadWrapper : FrameworkTestScene 21 { 22 private const int panel_count = 1024; 23 24 private FillFlowContainer<Container> flow; 25 private TestScrollContainer scroll; 26 27 [Resolved] 28 private Game game { get; set; } 29 30 [SetUp] 31 public void SetUp() => Schedule(() => 32 { 33 Children = new Drawable[] 34 { 35 scroll = new TestScrollContainer 36 { 37 RelativeSizeAxes = Axes.Both, 38 Children = new Drawable[] 39 { 40 flow = new FillFlowContainer<Container> 41 { 42 RelativeSizeAxes = Axes.X, 43 AutoSizeAxes = Axes.Y, 44 } 45 } 46 } 47 }; 48 }); 49 50 [Test] 51 public void TestUnloadViaScroll() 52 { 53 WeakList<Container> references = new WeakList<Container>(); 54 55 AddStep("populate panels", () => 56 { 57 references.Clear(); 58 59 for (int i = 0; i < 16; i++) 60 { 61 flow.Add(new Container 62 { 63 Size = new Vector2(128), 64 Children = new Drawable[] 65 { 66 new DelayedLoadUnloadWrapper(() => 67 { 68 var container = new Container 69 { 70 RelativeSizeAxes = Axes.Both, 71 Children = new Drawable[] 72 { 73 new TestBox { RelativeSizeAxes = Axes.Both } 74 }, 75 }; 76 77 references.Add(container); 78 79 return container; 80 }, 500, 2000), 81 new SpriteText { Text = i.ToString() }, 82 } 83 }); 84 } 85 86 flow.Add( 87 new Container 88 { 89 Size = new Vector2(128, 1280), 90 }); 91 }); 92 93 AddUntilStep("references loaded", () => references.Count() == 16 && references.All(c => c.IsLoaded)); 94 95 AddStep("scroll to end", () => scroll.ScrollToEnd()); 96 97 AddUntilStep("references lost", () => 98 { 99 GC.Collect(); 100 return !references.Any(); 101 }); 102 103 AddStep("scroll to start", () => scroll.ScrollToStart()); 104 105 AddUntilStep("references restored", () => references.Count() == 16); 106 } 107 108 [Test] 109 public void TestTasksCanceledDuringLoadSequence() 110 { 111 var references = new WeakList<TestBox>(); 112 113 AddStep("populate panels", () => 114 { 115 references.Clear(); 116 117 for (int i = 0; i < 16; i++) 118 { 119 DelayedLoadUnloadWrapper loadUnloadWrapper; 120 121 flow.Add(new Container 122 { 123 Size = new Vector2(128), 124 Child = loadUnloadWrapper = new DelayedLoadUnloadWrapper(() => 125 { 126 var content = new TestBox { RelativeSizeAxes = Axes.Both }; 127 references.Add(content); 128 return content; 129 }, 0), 130 }); 131 132 // cancel load tasks after the delayed load has started. 133 loadUnloadWrapper.DelayedLoadStarted += _ => game.Schedule(() => loadUnloadWrapper.UnbindAllBindables()); 134 } 135 }); 136 137 AddStep("remove all panels", () => flow.Clear(false)); 138 139 AddUntilStep("references lost", () => 140 { 141 GC.Collect(); 142 return !references.Any(); 143 }); 144 } 145 146 [Test] 147 public void TestRemovedStillUnload() 148 { 149 WeakList<Container> references = new WeakList<Container>(); 150 151 AddStep("populate panels", () => 152 { 153 references.Clear(); 154 155 for (int i = 0; i < 16; i++) 156 { 157 flow.Add(new Container 158 { 159 Size = new Vector2(128), 160 Children = new Drawable[] 161 { 162 new DelayedLoadUnloadWrapper(() => 163 { 164 var container = new Container 165 { 166 RelativeSizeAxes = Axes.Both, 167 Children = new Drawable[] 168 { 169 new TestBox { RelativeSizeAxes = Axes.Both } 170 }, 171 }; 172 173 references.Add(container); 174 175 return container; 176 }, 500, 2000), 177 new SpriteText { Text = i.ToString() }, 178 } 179 }); 180 } 181 }); 182 183 AddUntilStep("references loaded", () => references.Count() == 16 && references.All(c => c.IsLoaded)); 184 185 AddStep("Remove all panels", () => flow.Clear(false)); 186 187 AddUntilStep("references lost", () => 188 { 189 GC.Collect(); 190 return !references.Any(); 191 }); 192 } 193 194 [Test] 195 public void TestRemoveThenAdd() 196 { 197 WeakList<Container> references = new WeakList<Container>(); 198 199 int loadCount = 0; 200 201 AddStep("populate panels", () => 202 { 203 references.Clear(); 204 loadCount = 0; 205 206 for (int i = 0; i < 16; i++) 207 { 208 flow.Add(new Container 209 { 210 Size = new Vector2(128), 211 Children = new Drawable[] 212 { 213 new DelayedLoadUnloadWrapper(() => 214 { 215 TestBox testBox; 216 var container = new Container 217 { 218 RelativeSizeAxes = Axes.Both, 219 Children = new Drawable[] 220 { 221 testBox = new TestBox { RelativeSizeAxes = Axes.Both } 222 }, 223 }; 224 225 testBox.OnLoadComplete += _ => 226 { 227 references.Add(container); 228 loadCount++; 229 }; 230 231 return container; 232 }, 500, 2000), 233 new SpriteText { Text = i.ToString() }, 234 } 235 }); 236 } 237 }); 238 239 IReadOnlyList<Container> previousChildren = null; 240 241 AddUntilStep("all loaded", () => loadCount == 16); 242 243 AddStep("Remove all panels", () => 244 { 245 previousChildren = flow.Children.ToList(); 246 flow.Clear(false); 247 }); 248 249 AddStep("Add panels back", () => flow.Children = previousChildren); 250 251 AddWaitStep("wait for potential unload", 20); 252 253 AddAssert("load count hasn't changed", () => loadCount == 16); 254 } 255 256 [Test] 257 public void TestManyChildrenUnload() 258 { 259 int loaded = 0; 260 261 AddStep("populate panels", () => 262 { 263 loaded = 0; 264 265 for (int i = 1; i < panel_count; i++) 266 { 267 flow.Add(new Container 268 { 269 Size = new Vector2(128), 270 Children = new Drawable[] 271 { 272 new DelayedLoadUnloadWrapper(() => new Container 273 { 274 RelativeSizeAxes = Axes.Both, 275 Children = new Drawable[] 276 { 277 new TestBox(() => loaded++) { RelativeSizeAxes = Axes.Both } 278 } 279 }, 500, 2000), 280 new SpriteText { Text = i.ToString() }, 281 } 282 }); 283 } 284 }); 285 286 int childrenWithAvatarsLoaded() => 287 flow.Children.Count(c => c.Children.OfType<DelayedLoadWrapper>().First().Content?.IsLoaded ?? false); 288 289 int loadCount1 = 0; 290 int loadCount2 = 0; 291 292 AddUntilStep("wait for load", () => loaded > 0); 293 294 AddStep("scroll down", () => 295 { 296 loadCount1 = loaded; 297 scroll.ScrollToEnd(); 298 }); 299 300 AddUntilStep("more loaded", () => 301 { 302 loadCount2 = childrenWithAvatarsLoaded(); 303 return loaded > loadCount1; 304 }); 305 306 AddAssert("not too many loaded", () => childrenWithAvatarsLoaded() < panel_count / 4); 307 AddUntilStep("wait some unloaded", () => childrenWithAvatarsLoaded() < loadCount2); 308 } 309 310 [Test] 311 public void TestWrapperExpiry() 312 { 313 var wrappers = new List<DelayedLoadUnloadWrapper>(); 314 315 AddStep("populate panels", () => 316 { 317 for (int i = 1; i < 16; i++) 318 { 319 var wrapper = new DelayedLoadUnloadWrapper(() => new Container 320 { 321 RelativeSizeAxes = Axes.Both, 322 Children = new Drawable[] 323 { 324 new TestBox { RelativeSizeAxes = Axes.Both } 325 } 326 }, 500, 2000); 327 328 wrappers.Add(wrapper); 329 330 flow.Add(new Container 331 { 332 Size = new Vector2(128), 333 Children = new Drawable[] 334 { 335 wrapper, 336 new SpriteText { Text = i.ToString() }, 337 } 338 }); 339 } 340 }); 341 342 int childrenWithAvatarsLoaded() => flow.Children.Count(c => c.Children.OfType<DelayedLoadWrapper>().FirstOrDefault()?.Content?.IsLoaded ?? false); 343 344 AddUntilStep("wait some loaded", () => childrenWithAvatarsLoaded() > 5); 345 AddStep("expire wrappers", () => wrappers.ForEach(w => w.Expire())); 346 AddAssert("all unloaded", () => childrenWithAvatarsLoaded() == 0); 347 } 348 349 [Test] 350 public void TestUnloadWithNonOptimisingParent() 351 { 352 DelayedLoadUnloadWrapper wrapper = null; 353 354 AddStep("add panel", () => 355 { 356 Add(new Container 357 { 358 Anchor = Anchor.Centre, 359 Origin = Anchor.Centre, 360 Size = new Vector2(128), 361 Masking = true, 362 Child = wrapper = new DelayedLoadUnloadWrapper(() => new TestBox { RelativeSizeAxes = Axes.Both }, 0, 1000) 363 }); 364 }); 365 366 AddUntilStep("wait for load", () => wrapper.Content?.IsLoaded == true); 367 AddStep("move wrapper outside", () => wrapper.X = 129); 368 AddUntilStep("wait for unload", () => wrapper.Content?.IsLoaded != true); 369 } 370 371 [Test] 372 public void TestUnloadWithOffscreenParent() 373 { 374 Container parent = null; 375 DelayedLoadUnloadWrapper wrapper = null; 376 377 AddStep("add panel", () => 378 { 379 Add(parent = new Container 380 { 381 Anchor = Anchor.Centre, 382 Origin = Anchor.Centre, 383 Size = new Vector2(128), 384 Masking = true, 385 Child = wrapper = new DelayedLoadUnloadWrapper(() => new TestBox { RelativeSizeAxes = Axes.Both }, 0, 1000) 386 }); 387 }); 388 389 AddUntilStep("wait for load", () => wrapper.Content?.IsLoaded == true); 390 AddStep("move parent offscreen", () => parent.X = 1000000); // Should be offscreen 391 AddUntilStep("wait for unload", () => wrapper.Content?.IsLoaded != true); 392 } 393 394 [Test] 395 public void TestUnloadWithParentRemovedFromHierarchy() 396 { 397 Container parent = null; 398 DelayedLoadUnloadWrapper wrapper = null; 399 400 AddStep("add panel", () => 401 { 402 Add(parent = new Container 403 { 404 Anchor = Anchor.Centre, 405 Origin = Anchor.Centre, 406 Size = new Vector2(128), 407 Masking = true, 408 Child = wrapper = new DelayedLoadUnloadWrapper(() => new TestBox { RelativeSizeAxes = Axes.Both }, 0, 1000) 409 }); 410 }); 411 412 AddUntilStep("wait for load", () => wrapper.Content?.IsLoaded == true); 413 AddStep("remove parent", () => Remove(parent)); 414 AddUntilStep("wait for unload", () => wrapper.Content?.IsLoaded != true); 415 } 416 417 [Test] 418 public void TestUnloadedWhenAsyncLoadCompletedAndMaskedAway() 419 { 420 BasicScrollContainer scrollContainer = null; 421 DelayedLoadTestDrawable child = null; 422 423 AddStep("add panel", () => 424 { 425 Child = scrollContainer = new BasicScrollContainer 426 { 427 Anchor = Anchor.Centre, 428 Origin = Anchor.Centre, 429 Size = new Vector2(128), 430 Child = new Container 431 { 432 RelativeSizeAxes = Axes.X, 433 Height = 1000, 434 Child = new Container 435 { 436 RelativeSizeAxes = Axes.X, 437 Height = 128, 438 Child = new DelayedLoadUnloadWrapper(() => child = new DelayedLoadTestDrawable { RelativeSizeAxes = Axes.Both }, 0, 1000) 439 { 440 RelativeSizeAxes = Axes.X, 441 Height = 128 442 } 443 } 444 } 445 }; 446 }); 447 448 // Check that the child is disposed when its async-load completes while the wrapper is masked away. 449 AddAssert("wait for load to begin", () => child?.LoadState == LoadState.Loading); 450 AddStep("scroll to end", () => scrollContainer.ScrollToEnd(false)); 451 AddStep("allow load", () => child.AllowLoad.Set()); 452 AddUntilStep("drawable disposed", () => child.IsDisposed); 453 454 Drawable lastChild = null; 455 AddStep("store child", () => lastChild = child); 456 457 // Check that reuse of the child is not attempted. 458 AddStep("scroll to start", () => scrollContainer.ScrollToStart(false)); 459 AddStep("allow load of new child", () => child.AllowLoad.Set()); 460 AddUntilStep("new child loaded", () => child.IsLoaded); 461 AddAssert("last child not loaded", () => !lastChild.IsLoaded); 462 } 463 464 [Test] 465 public void TestWrapperStopReceivingUpdatesAfterDelayedLoadCompleted() 466 { 467 DelayedLoadTestDrawable child = null; 468 469 AddStep("add panel", () => 470 { 471 DelayedLoadUnloadWrapper wrapper; 472 473 Child = wrapper = new DelayedLoadUnloadWrapper(() => child = new DelayedLoadTestDrawable { RelativeSizeAxes = Axes.Both }, 0, 1000) 474 { 475 RelativeSizeAxes = Axes.X, 476 Height = 128, 477 }; 478 479 // Prevent the wrapper from receiving updates as soon as load completes, and start making it unload its contents by repositioning it offscreen. 480 wrapper.DelayedLoadComplete += _ => 481 { 482 wrapper.Alpha = 0; 483 wrapper.Position = new Vector2(-1000); 484 }; 485 }); 486 487 // Check that the child is disposed when its async-load completes while the wrapper is masked away. 488 AddAssert("wait for load to begin", () => child?.LoadState == LoadState.Loading); 489 AddStep("allow load", () => child.AllowLoad.Set()); 490 AddUntilStep("drawable disposed", () => child.IsDisposed); 491 } 492 493 public class TestScrollContainer : BasicScrollContainer 494 { 495 public new Scheduler Scheduler => base.Scheduler; 496 } 497 498 public class TestBox : Container 499 { 500 private readonly Action onLoadAction; 501 502 public TestBox(Action onLoadAction = null) 503 { 504 this.onLoadAction = onLoadAction; 505 RelativeSizeAxes = Axes.Both; 506 } 507 508 [BackgroundDependencyLoader] 509 private void load() 510 { 511 onLoadAction?.Invoke(); 512 513 Child = new SpriteText 514 { 515 Colour = Color4.Yellow, 516 Text = @"loaded", 517 Anchor = Anchor.Centre, 518 Origin = Anchor.Centre, 519 }; 520 } 521 } 522 523 public class DelayedLoadTestDrawable : CompositeDrawable 524 { 525 public readonly ManualResetEventSlim AllowLoad = new ManualResetEventSlim(false); 526 527 [BackgroundDependencyLoader] 528 private void load() 529 { 530 if (!AllowLoad.Wait(TimeSpan.FromSeconds(10))) 531 throw new TimeoutException(); 532 } 533 } 534 } 535}