Skip to content

Commit da41f94

Browse files
perf: replace O(n log n) OrderBy with O(n) reverse enumeration in HitObjectContainer; replace Single/SingleOrDefault with First/FirstOrDefault; fix OnlineStatusNotifier bindable leak
Agent-Logs-Url: https://github.com/winnerspiros/osu/sessions/b0878070-74cc-41e1-af2d-af6fb8954594 Co-authored-by: winnerspiros <1675249+winnerspiros@users.noreply.github.com>
1 parent 6537aec commit da41f94

5 files changed

Lines changed: 42 additions & 5 deletions

File tree

osu.Game/Online/OnlineStatusNotifier.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,10 @@ protected override void Dispose(bool isDisposing)
161161
{
162162
base.Dispose(isDisposing);
163163

164+
apiState?.UnbindAll();
165+
multiplayerState?.UnbindAll();
166+
spectatorState?.UnbindAll();
167+
164168
if (notificationsClient.IsNotNull())
165169
notificationsClient.MessageReceived -= notifyAboutForcedDisconnection;
166170

osu.Game/Rulesets/UI/DrawableRuleset.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,7 @@ public bool RemoveHitObject(TObject hitObject)
271271
return true;
272272

273273
// If the entry was not removed from the playfield, assume the hitobject is not being pooled and attempt a direct drawable removal.
274-
var drawableObject = Playfield.AllHitObjects.SingleOrDefault(d => d.HitObject == hitObject);
274+
var drawableObject = Playfield.AllHitObjects.FirstOrDefault(d => d.HitObject == hitObject);
275275
if (drawableObject != null)
276276
return Playfield.Remove(drawableObject);
277277

osu.Game/Rulesets/UI/HitObjectContainer.cs

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,42 @@ namespace osu.Game.Rulesets.UI
1919
{
2020
public partial class HitObjectContainer : PooledDrawableWithLifetimeContainer<HitObjectLifetimeEntry, DrawableHitObject>, IHitObjectContainer
2121
{
22-
public IEnumerable<DrawableHitObject> Objects => InternalChildren.Cast<DrawableHitObject>().OrderBy(h => h.HitObject.StartTime);
22+
/// <summary>
23+
/// All <see cref="DrawableHitObject"/>s in this container, sorted by ascending <see cref="HitObject.StartTime"/>.
24+
/// </summary>
25+
/// <remarks>
26+
/// Since internal children are already sorted by descending <see cref="HitObject.StartTime"/>
27+
/// (via <see cref="Compare"/>), we reverse-enumerate to avoid an O(n log n) sort on every access.
28+
/// </remarks>
29+
public IEnumerable<DrawableHitObject> Objects => enumerateByStartTimeAscending();
30+
31+
/// <summary>
32+
/// All alive <see cref="DrawableHitObject"/>s in this container, sorted by ascending <see cref="HitObject.StartTime"/>.
33+
/// </summary>
34+
/// <remarks>
35+
/// The alive entries dictionary is unordered, so we must sort.
36+
/// However, the alive set is typically much smaller than the full set, making this cheaper
37+
/// than sorting all children. We use a List + Sort (in-place) to avoid LINQ iterator allocations.
38+
/// </remarks>
39+
public IEnumerable<DrawableHitObject> AliveObjects => getSortedAliveObjects();
40+
41+
private IEnumerable<DrawableHitObject> enumerateByStartTimeAscending()
42+
{
43+
var children = InternalChildren;
44+
45+
for (int i = children.Count - 1; i >= 0; i--)
46+
{
47+
if (children[i] is DrawableHitObject hitObject)
48+
yield return hitObject;
49+
}
50+
}
2351

24-
public IEnumerable<DrawableHitObject> AliveObjects => AliveEntries.Values.OrderBy(h => h.HitObject.StartTime);
52+
private List<DrawableHitObject> getSortedAliveObjects()
53+
{
54+
var list = new List<DrawableHitObject>(AliveEntries.Values);
55+
list.Sort(static (a, b) => a.HitObject.StartTime.CompareTo(b.HitObject.StartTime));
56+
return list;
57+
}
2558

2659
/// <summary>
2760
/// Invoked when a <see cref="DrawableHitObject"/> is judged.

osu.Game/Screens/Edit/Compose/HitObjectUsageEventBuffer.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ public HitObjectUsageEventBuffer([NotNull] Playfield playfield)
5959
private void onHitObjectUsageBegan(HitObject hitObject)
6060
{
6161
if (usageFinishedHitObjects.Remove(hitObject))
62-
HitObjectUsageTransferred?.Invoke(hitObject, playfield.AllHitObjects.Single(d => d.HitObject == hitObject));
62+
HitObjectUsageTransferred?.Invoke(hitObject, playfield.AllHitObjects.First(d => d.HitObject == hitObject));
6363
else
6464
HitObjectUsageBegan?.Invoke(hitObject);
6565
}

osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ void preventMiss(HitObject hitObject)
131131
{
132132
var drawableObject = DrawableRuleset.Playfield.HitObjectContainer
133133
.AliveObjects
134-
.SingleOrDefault(it => it.HitObject == hitObject);
134+
.FirstOrDefault(it => it.HitObject == hitObject);
135135

136136
if (drawableObject != null)
137137
preventMissOnDrawable(drawableObject);

0 commit comments

Comments
 (0)