forked from ppy/osu-framework
-
Notifications
You must be signed in to change notification settings - Fork 0
/
DrawablePool.cs
248 lines (197 loc) · 8.31 KB
/
DrawablePool.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
using osu.Framework.Allocation;
using osu.Framework.Extensions.TypeExtensions;
using osu.Framework.Graphics.Containers;
using osu.Framework.Statistics;
namespace osu.Framework.Graphics.Pooling
{
/// <summary>
/// A component which provides a pool of reusable drawables.
/// Should be used to reduce allocation and construction overhead of individual drawables.
/// </summary>
/// <remarks>
/// The <see cref="initialSize"/> drawables will be prepared ahead-of-time during this pool's asynchronous load procedure.
/// Drawables exceeding the pool's available size will not be asynchronously loaded as it is assumed they are immediately required for consumption.
/// </remarks>
/// <typeparam name="T">The type of drawable to be pooled.</typeparam>
public partial class DrawablePool<T> : CompositeDrawable, IDrawablePool where T : PoolableDrawable, new()
{
private GlobalStatistic<DrawablePoolUsageStatistic> statistic;
private readonly int initialSize;
private readonly int? maximumSize;
private readonly Stack<T> pool = new Stack<T>();
// ReSharper disable once StaticMemberInGenericType (this is intentional, we want a separate count per type).
private static int poolInstanceID;
/// <summary>
/// Create a new pool instance.
/// </summary>
/// <param name="initialSize">The number of drawables to be prepared for initial consumption.</param>
/// <param name="maximumSize">An optional maximum size after which the pool will no longer be expanded.</param>
public DrawablePool(int initialSize, int? maximumSize = null)
{
if (initialSize > maximumSize)
throw new ArgumentOutOfRangeException(nameof(initialSize), "Initial size must be less than or equal to maximum size.");
this.maximumSize = maximumSize;
this.initialSize = initialSize;
int id = Interlocked.Increment(ref poolInstanceID);
statistic = GlobalStatistics.Get<DrawablePoolUsageStatistic>(nameof(DrawablePool<T>), typeof(T).ReadableName() + $"`{id}");
statistic.Value = new DrawablePoolUsageStatistic();
}
[BackgroundDependencyLoader]
private void load()
{
for (int i = 0; i < initialSize; i++)
pool.Push(create());
CurrentPoolSize = initialSize;
LoadComponents(pool.ToArray());
}
/// <summary>
/// Return a drawable after use.
/// </summary>
/// <param name="pooledDrawable">The drawable to return. Should have originally come from this pool.</param>
public void Return(PoolableDrawable pooledDrawable)
{
if (pooledDrawable is not T typedDrawable)
throw new ArgumentException("Invalid type", nameof(pooledDrawable));
if (pooledDrawable.Parent != null)
throw new InvalidOperationException("Drawable was attempted to be returned to pool while still in a hierarchy");
if (pooledDrawable.IsInUse)
{
// if the return operation didn't come from the drawable, redirect to ensure consistent behaviour.
pooledDrawable.Return();
return;
}
//TODO: check the drawable was sourced from this pool for safety.
if (CountAvailable >= maximumSize)
{
// if the drawable can't be returned to the pool, mark it as such so it can be disposed of.
pooledDrawable.SetPool(null);
// then attempt disposal.
if (pooledDrawable.DisposeOnDeathRemoval)
DisposeChildAsync(pooledDrawable);
}
else
{
pool.Push(typedDrawable);
}
CountInUse--;
}
PoolableDrawable IDrawablePool.Get(Action<PoolableDrawable> setupAction) => Get(setupAction);
/// <summary>
/// Get a drawable from this pool.
/// </summary>
/// <param name="setupAction">An optional action to be performed on this drawable immediately after retrieval. Should generally be used to prepare the drawable into a usable state.</param>
/// <returns>The drawable.</returns>
public T Get(Action<T> setupAction = null)
{
if (LoadState <= LoadState.Loading)
throw new InvalidOperationException($"A {nameof(DrawablePool<T>)} must be in a loaded state before retrieving pooled drawables.");
if (!pool.TryPop(out var drawable))
{
drawable = create();
if (maximumSize == null || currentPoolSize < maximumSize)
{
CurrentPoolSize++;
Debug.Assert(maximumSize == null || CurrentPoolSize <= maximumSize);
}
else
CountExcessConstructed++;
if (LoadState >= LoadState.Loading)
LoadComponent(drawable);
}
CountInUse++;
drawable.Assign();
drawable.LifetimeStart = double.MinValue;
drawable.LifetimeEnd = double.MaxValue;
setupAction?.Invoke(drawable);
return drawable;
}
/// <summary>
/// Create a new drawable to be consumed or added to the pool.
/// </summary>
protected virtual T CreateNewDrawable() => new T();
private T create()
{
var drawable = CreateNewDrawable();
drawable.SetPool(this);
return drawable;
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
foreach (var p in pool)
p.Dispose();
CountInUse = 0;
CountExcessConstructed = 0;
CurrentPoolSize = 0;
GlobalStatistics.Remove(statistic);
// Disallow any further Gets/Returns to adjust the statistics.
statistic = null;
}
private int currentPoolSize;
/// <summary>
/// The current size of the pool.
/// </summary>
public int CurrentPoolSize
{
get => currentPoolSize;
private set
{
Debug.Assert(statistic != null);
statistic.Value.CurrentPoolSize = currentPoolSize = value;
}
}
private int countInUse;
/// <summary>
/// The number of drawables currently in use.
/// </summary>
public int CountInUse
{
get => countInUse;
private set
{
Debug.Assert(statistic != null);
statistic.Value.CountInUse = countInUse = value;
}
}
private int countExcessConstructed;
/// <summary>
/// The total number of drawables constructed that were not pooled.
/// </summary>
public int CountExcessConstructed
{
get => countExcessConstructed;
private set
{
Debug.Assert(statistic != null);
statistic.Value.CountExcessConstructed = countExcessConstructed = value;
}
}
/// <summary>
/// The number of drawables currently available for consumption.
/// </summary>
public int CountAvailable => pool.Count;
private class DrawablePoolUsageStatistic
{
/// <summary>
/// Total number of drawables available for use (in the pool).
/// </summary>
public int CurrentPoolSize;
/// <summary>
/// Total number of drawables currently in use.
/// </summary>
public int CountInUse;
/// <summary>
/// Total number of drawables constructed that were not pooled.
/// </summary>
public int CountExcessConstructed;
public override string ToString() => $"{CountInUse}/{CurrentPoolSize} ({CountExcessConstructed} excess)";
}
}
}