perf: second-pass hot-path audit — MathF, DimmablePieces cache, Taiko loop - #308
Merged
Merged
Conversation
…ection in multiplayer (ppy#37646) Fixes a bug that allowed for selecting the same mod twice in multiplayer if the playlist entry has non-default settings for a required mod. This happens due to a strict equality mod check in the mod set compatibility check function, which only considers mods duplicates if their settings are exactly the same. Replacing with a more lenient `Type` check fixes this. Adds a regression test for this behavior Fixes ppy#37625. --------- Co-authored-by: Bartłomiej Dach <dach.bartlomiej@gmail.com>
…e screen (ppy#37658) Until now the queue screen basically did nothing to let the user knowing they were disconnected from the server. Now the various components will correctly clear state and show a roughly competent "i'm trying to reconnect" state. https://github.com/user-attachments/assets/bff1b241-a6a2-445a-9ffa-b5682f2a3656 --- Can be tested using the following patch (hit `F7` to reconnect, with a 5 second delay to show the disconnected state too): ```diff diff --git a/osu.Game/Online/PersistentEndpointClientConnector.cs b/osu.Game/Online/PersistentEndpointClientConnector.cs index 7064906..ae539aba8d 100644 --- a/osu.Game/Online/PersistentEndpointClientConnector.cs +++ b/osu.Game/Online/PersistentEndpointClientConnector.cs @@ -99,6 +99,8 @@ private async Task connect() // this will also create a new cancellation token source. await disconnect(false).ConfigureAwait(false); + await Task.Delay(5000).ConfigureAwait(false); + // this token will be valid for the scope of this connection. // if cancelled, we can be sure that a disconnect or reconnect is handled elsewhere. var cancellationToken = connectCancelSource.Token; diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 703444a..fb467472d3 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -22,6 +22,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Textures; using osu.Framework.Input; +using osu.Framework.Input.Events; using osu.Framework.Input.Handlers; using osu.Framework.Input.Handlers.Joystick; using osu.Framework.Input.Handlers.Midi; @@ -65,6 +66,7 @@ using osu.Game.Scoring; using osu.Game.Skinning; using osu.Game.Utils; +using osuTK.Input; using RuntimeInfo = osu.Framework.RuntimeInfo; namespace osu.Game @@ -104,7 +106,7 @@ public partial class OsuGameBase : Framework.Game, ICanAcceptFiles, IBeatSyncPro /// </summary> private const double global_track_volume_adjust = 0.8; - public virtual bool UseDevelopmentServer => DebugUtils.IsDebugBuild; + public virtual bool UseDevelopmentServer => false; public virtual EndpointConfiguration CreateEndpoints() => UseDevelopmentServer ? new DevelopmentEndpointConfiguration() : new ProductionEndpointConfiguration(); @@ -466,6 +468,20 @@ private void addFilesWarning() } } + protected override bool OnKeyDown(KeyDownEvent e) + { + if (e.Key == Key.F7) + { + Logger.Log("Forcing reconnect!", level: LogLevel.Important); + + ((IStatefulUserHubClient)MultiplayerClient).ServerShuttingDown(); + ((IStatefulUserHubClient)SpectatorClient).ServerShuttingDown(); + ((IStatefulUserHubClient)metadataClient).ServerShuttingDown(); + } + + return base.OnKeyDown(e); + } + private void onTrackChanged(WorkingBeatmap beatmap, TrackChangeDirection direction) => beatmapClock.ChangeSource(beatmap.Track); protected virtual void InitialiseFonts() ```
Matches stable. --- Addresses ppy#37580.
…BDSP 2-min sleep, MMAP StabilizedCallback skip, OsuGame BDSP factory Agent-Logs-Url: https://github.com/winnerspiros/osu/sessions/4160543d-58b3-40e7-a293-c38a0b6fba4f Co-authored-by: winnerspiros <1675249+winnerspiros@users.noreply.github.com>
…enQueue dup-handler fix, test type fix, mod utils) Agent-Logs-Url: https://github.com/winnerspiros/osu/sessions/4160543d-58b3-40e7-a293-c38a0b6fba4f Co-authored-by: winnerspiros <1675249+winnerspiros@users.noreply.github.com>
…r, SliderInputManager, DrawableSpinner, BeatmapCarousel, OsuGameAndroid, GameplaySampleTriggerSource, DrawableHitObject, oboe_bridge) Agent-Logs-Url: https://github.com/winnerspiros/osu/sessions/49b81aac-c71a-4671-b650-5a258c6b1baf Co-authored-by: winnerspiros <1675249+winnerspiros@users.noreply.github.com>
…eSlider covariance, Catcher.computePositionInStack manual loop) Agent-Logs-Url: https://github.com/winnerspiros/osu/sessions/60401bc7-7f04-4693-b4fb-40a5851d015a Co-authored-by: winnerspiros <1675249+winnerspiros@users.noreply.github.com>
- HitObjectContainer.AliveObjects: replace per-call new List + Sort with a dirty-flagged persistent cache. The list is rebuilt only when addDrawable / removeDrawable fires, eliminating allocations on every hit-policy check and every frame in LegacyCursorParticles/OsuModRelax (HIGH impact). - DrawableHitCircle.DimmablePieces: cache the single-element Drawable[] as a field assigned in load() instead of allocating a new array on each property access from UpdateInitialTransforms / ClearNestedHitObjects (MEDIUM impact). - DrawableOsuHitObject.ClearNestedHitObjects: replace DimmablePieces.OfType<>() LINQ iterator with a plain foreach + 'is DrawableHitObject' pattern-match, eliminating the LINQ state-machine allocation on every pool return (MEDIUM). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: winnerspiros <1675249+winnerspiros@users.noreply.github.com>
…trictTracking/Bubbles hot paths Agent-Logs-Url: https://github.com/winnerspiros/osu/sessions/60401bc7-7f04-4693-b4fb-40a5851d015a Co-authored-by: winnerspiros <1675249+winnerspiros@users.noreply.github.com>
Copilot created this pull request from a session on behalf of
winnerspiros
May 7, 2026 17:17
View session
winnerspiros
approved these changes
May 7, 2026
… touch/hit-policy/relax/taiko allocs) Agent-Logs-Url: https://github.com/winnerspiros/osu/sessions/16daf401-e98b-42e6-8464-4745383d6e40 Co-authored-by: winnerspiros <1675249+winnerspiros@users.noreply.github.com>
winnerspiros
approved these changes
May 7, 2026
…edundant casts, unused usings Agent-Logs-Url: https://github.com/winnerspiros/osu/sessions/45232cd1-6884-4c60-815e-dba288232c8c Co-authored-by: winnerspiros <1675249+winnerspiros@users.noreply.github.com>
winnerspiros
approved these changes
May 7, 2026
Agent-Logs-Url: https://github.com/winnerspiros/osu/sessions/7ba7b342-765f-428b-96e1-3925b8e2d9ab Co-authored-by: winnerspiros <1675249+winnerspiros@users.noreply.github.com>
winnerspiros
approved these changes
May 8, 2026
…ource gen warning) Agent-Logs-Url: https://github.com/winnerspiros/osu/sessions/e95ab934-869c-4165-ad22-563b0f51c6b8 Co-authored-by: winnerspiros <1675249+winnerspiros@users.noreply.github.com>
winnerspiros
approved these changes
May 8, 2026
… ISampleInfo[]) Agent-Logs-Url: https://github.com/winnerspiros/osu/sessions/258720a5-84bc-4283-b524-809219539b6a Co-authored-by: winnerspiros <1675249+winnerspiros@users.noreply.github.com>
winnerspiros
approved these changes
May 8, 2026
…ISampleInfo) Agent-Logs-Url: https://github.com/winnerspiros/osu/sessions/6590546f-8ba3-432c-af4d-2128d82c40f4 Co-authored-by: winnerspiros <1675249+winnerspiros@users.noreply.github.com>
winnerspiros
approved these changes
May 8, 2026
…ectCode warning) Agent-Logs-Url: https://github.com/winnerspiros/osu/sessions/3392b526-2e9e-4809-aa6c-cb6622e9295d Co-authored-by: winnerspiros <1675249+winnerspiros@users.noreply.github.com>
winnerspiros
approved these changes
May 8, 2026
There was a problem hiding this comment.
Pull request overview
This PR performs a second-pass performance audit across gameplay and platform hot paths, primarily reducing per-frame/per-judgement allocations (LINQ/state-machine removal) and adding a platform override point for background data processing.
Changes:
- Replaces several LINQ queries in hot paths with manual loops / cached results (gameplay, scoring, UI, ruleset logic).
- Adds a factory method for
BackgroundDataStoreProcessorand provides an Android-specific override with a longer gameplay sleep interval. - Adjusts Android native audio stream opening to avoid
StabilizedCallbackoverhead on MMAP paths, while keeping it for non-MMAP fallbacks.
Reviewed changes
Copilot reviewed 23 out of 23 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| osu.Game/Screens/Select/BeatmapCarouselFilterSorting.cs | Reworks aggregate max computation to a single-pass helper for hidden-beatmap filtering. |
| osu.Game/Screens/Play/ReplayPlayer.cs | Caches autoplay detection to avoid repeated LINQ in fail-check path. |
| osu.Game/Screens/Play/HUD/HitErrorMeters/BarHitErrorMeter.cs | Replaces FirstOrDefault LINQ with manual scan on judgement callback. |
| osu.Game/Screens/OnlinePlay/Matchmaking/Queue/ScreenQueue.cs | Diff indicates event subscription adjustment in LoadComplete(). |
| osu.Game/Rulesets/UI/HitObjectContainer.cs | Introduces cached sorted alive-object list to avoid per-access allocations. |
| osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs | Converts nested hitobject traversal to iterative scratch-list approach; modifies sample conversion in Play(). |
| osu.Game/Rulesets/Scoring/ScoreProcessor.cs | Caches applicable score mods array to avoid per-judgement LINQ allocation in rank updates. |
| osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs | Avoids redundant sample array conversion by reusing a single converted array. |
| osu.Game/OsuGame.cs | Adds CreateBackgroundDataStoreProcessor() override point and uses it during load. |
| osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs | Adjusts test call to clear user mods using APIMod empty array. |
| osu.Game.Rulesets.Osu/UI/LegacyHitPolicy.cs | Removes list copying in note-lock logic by single-pass enumeration. |
| osu.Game.Rulesets.Osu/Objects/Drawables/SliderInputManager.cs | Removes OfType()/LastOrDefault() LINQ in slider nested-object handling. |
| osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs | Removes LINQ from end-of-spinner and bonus tick selection paths. |
| osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs | Removes LINQ from tick counting / any-hit checks; addresses formatting warning. |
| osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs | Removes LINQ OfType() from dimmable-piece unsubscribe path. |
| osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs | Caches DimmablePieces array to avoid per-access array allocation. |
| osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs | Replaces OfType().First() with manual scan on tracking-change path. |
| osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs | Removes per-frame OfType() LINQ in relax mod update loop. |
| osu.Game.Rulesets.Osu/Mods/OsuModBubbles.cs | Replaces LINQ OfType().LastOrDefault() with reverse scan. |
| osu.Game.Rulesets.Catch/UI/Catcher.cs | Removes LINQ Any() from stacking loop by extracting a manual predicate scan. |
| osu.Android/OsuGameAndroid.cs | Caches ADPF input reporting interval; wires Android background processor override. |
| osu.Android/Native/oboe_bridge.cpp | Two-pass stream open to avoid stabilization on MMAP; minor callback-side micro-optimisations. |
| osu.Android/AndroidBackgroundDataStoreProcessor.cs | New Android-specific BackgroundDataStoreProcessor with extended gameplay sleep interval. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+63
to
+66
| aliveObjectsSortedCache.Sort(static (a, b) => a.HitObject.StartTime.CompareTo(b.HitObject.StartTime)); | ||
| aliveObjectsCacheDirty = false; | ||
| } | ||
|
|
Comment on lines
+153
to
+168
| /// <summary> | ||
| /// Returns the maximum value of <paramref name="func"/> over all non-hidden beatmaps in | ||
| /// <paramref name="beatmaps"/>, or <see cref="double.NegativeInfinity"/> if every beatmap | ||
| /// is hidden. Single-pass, allocation-free (no LINQ enumerator). | ||
| /// </summary> | ||
| private static double aggregateMax(IList<BeatmapInfo> beatmaps, Func<BeatmapInfo, double> func) | ||
| { | ||
| double max = double.NegativeInfinity; | ||
|
|
||
| foreach (var b in beatmaps) | ||
| { | ||
| if (b.Hidden) continue; | ||
|
|
||
| double v = func(b); | ||
| if (v > max) max = v; | ||
| } |
Comment on lines
+71
to
73
| PlaySamples(nextObject.Samples.Cast<ISampleInfo>().ToArray()); | ||
| } | ||
|
|
Comment on lines
151
to
175
| // Else we want the earliest valid nested. | ||
| // In cases of nested objects, they will always have earlier sample data than their parent object. | ||
| // Single-pass scan avoids the OrderBy + SkipWhile + FirstOrDefault LINQ chain. | ||
| // Iterative DFS with a shared scratch list avoids per-call state-machine allocations from recursive yield return. | ||
| double referenceTime = getReferenceTime(); | ||
| HitObject? best = null; | ||
| double bestEnd = double.MaxValue; | ||
|
|
||
| foreach (var nested in getAllNested(mostValidObject.HitObject)) | ||
| nestedScratch.Clear(); | ||
| nestedScratch.AddRange(mostValidObject.HitObject.NestedHitObjects); | ||
|
|
||
| for (int i = 0; i < nestedScratch.Count; i++) | ||
| { | ||
| var nested = nestedScratch[i]; | ||
| double end = nested.GetEndTime(); | ||
|
|
||
| if (end > referenceTime && end < bestEnd) | ||
| { | ||
| best = nested; | ||
| bestEnd = end; | ||
| } | ||
|
|
||
| // Enqueue children for depth-first traversal. | ||
| if (nested.NestedHitObjects.Count > 0) | ||
| nestedScratch.AddRange(nested.NestedHitObjects); | ||
| } |
Comment on lines
+40
to
+42
| // AliveObjects already returns a new sorted List<T> from getSortedAliveObjects(). | ||
| // Calling .ToList() on the IEnumerable<> would copy that list a second time. | ||
| // Single-pass over the enumerable uses only the one allocation that sorting requires. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
DrawableSlider.cs— wrapforeachbody in braces and putif (h.IsHit)body on its own line