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 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}